0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-08 02:52:39 -05:00

Changes for Koenig and Ghost 2.0 (#9750)

refs #9742, refs #9724

- handle König Editor format for 2.0
- adapted importer to be able to import 1.0 and 2.0 exports
- added migration scripts
  - remove labs flag for Koenig
  - migrate all old editor posts to new editor format
- ensure we protect the code against mobiledoc or html field being null
- ensure we create a blank mobiledoc structure if mobiledoc field is null (model layer)
- ensure you can fully rollback 2.0 to 1.0
- keep mobiledoc/markdown version 1 logic to be able to rollback (deprecated code)
This commit is contained in:
Katharina Irrgang 2018-08-03 13:02:14 +02:00
parent ef5dd6b878
commit c39df004dc
15 changed files with 239 additions and 214 deletions

View file

@ -2,6 +2,7 @@ const debug = require('ghost-ignition').debug('importer:posts'),
_ = require('lodash'),
uuid = require('uuid'),
BaseImporter = require('./base'),
converters = require('../../../../lib/mobiledoc/converters'),
validation = require('../../../validation');
class PostsImporter extends BaseImporter {
@ -160,6 +161,33 @@ class PostsImporter extends BaseImporter {
model.comment_id = model.id;
}
}
// CASE 1: you are importing old editor posts
// CASE 2: you are importing Koenig Beta posts
if (model.mobiledoc || (model.mobiledoc && model.html && model.html.match(/^<div class="kg-card-markdown">/))) {
let mobiledoc;
try {
mobiledoc = JSON.parse(model.mobiledoc);
if (!mobiledoc.cards || !_.isArray(mobiledoc.cards)) {
model.mobiledoc = converters.mobiledocConverter.blankStructure();
mobiledoc = model.mobiledoc;
}
} catch (err) {
mobiledoc = converters.mobiledocConverter.blankStructure();
}
mobiledoc.cards.forEach((card) => {
if (card[0] === 'image') {
card[1].cardWidth = card[1].imageStyle;
delete card[1].imageStyle;
}
});
model.mobiledoc = JSON.stringify(mobiledoc);
model.html = converters.mobiledocConverter.render(JSON.parse(model.mobiledoc));
}
});
// NOTE: We only support removing duplicate posts within the file to import.

View file

@ -2,19 +2,38 @@ const _ = require('lodash'),
Promise = require('bluebird'),
common = require('../../../../lib/common'),
models = require('../../../../models'),
message1 = 'Updating post data (comment_id)',
message2 = 'Updated post data (comment_id)',
message3 = 'Rollback: Keep correct comment_id values in amp column.';
converters = require('../../../../lib/mobiledoc/converters'),
message1 = 'Updating posts: apply new editor format and set comment_id field.',
message2 = 'Updated posts: apply new editor format and set comment_id field.',
message3 = 'Rollback: Updating posts: use old editor format',
message4 = 'Rollback: Updated posts: use old editor format';
module.exports.config = {
transaction: true
};
let mobiledocIsCompatibleWithV1 = function mobiledocIsCompatibleWithV1(doc) {
if (doc
&& doc.markups.length === 0
&& doc.cards.length === 1
&& doc.cards[0][0].match(/(?:card-)?markdown/)
&& doc.sections.length === 1
&& doc.sections[0].length === 2
&& doc.sections[0][0] === 10
&& doc.sections[0][1] === 0
) {
return true;
}
return false;
};
module.exports.up = (options) => {
const postAllColumns = ['id', 'comment_id'];
const postAllColumns = ['id', 'comment_id', 'html', 'mobiledoc'];
let localOptions = _.merge({
context: {internal: true}
context: {internal: true},
migrating: true
}, options);
common.logging.info(message1);
@ -22,12 +41,27 @@ module.exports.up = (options) => {
return models.Post.findAll(_.merge({columns: postAllColumns}, localOptions))
.then(function (posts) {
return Promise.map(posts.models, function (post) {
if (post.get('comment_id')) {
return Promise.resolve();
let mobiledoc;
let html;
try {
mobiledoc = JSON.parse(post.get('mobiledoc') || null);
} catch (err) {
common.logging.warn(`Invalid mobiledoc structure for ${post.id}. Falling back to blank structure.`);
mobiledoc = converters.mobiledocConverter.blankStructure();
}
// CASE: convert all old editor posts to the new editor format
// CASE: if mobiledoc field is null, we auto set a blank structure in the model layer
// CASE: if html field is null, we auto generate the html in the model layer
if (mobiledoc && post.get('html') && post.get('html').match(/^<div class="kg-card-markdown">/)) {
html = converters.mobiledocConverter.render(mobiledoc);
}
return models.Post.edit({
comment_id: post.id
comment_id: post.get('comment_id') || post.id,
html: html || post.get('html'),
mobiledoc: JSON.stringify(mobiledoc)
}, _.merge({id: post.id}, localOptions));
}, {concurrency: 100});
})
@ -36,7 +70,38 @@ module.exports.up = (options) => {
});
};
module.exports.down = () => {
common.logging.warn(message3);
return Promise.resolve();
module.exports.down = (options) => {
const postAllColumns = ['id', 'html', 'mobiledoc'];
let localOptions = _.merge({
context: {internal: true},
migrating: true
}, options);
common.logging.info(message3);
return models.Post.findAll(_.merge({columns: postAllColumns}, localOptions))
.then(function (posts) {
return Promise.map(posts.models, function (post) {
let version = 1;
let html;
let mobiledoc = JSON.parse(post.get('mobiledoc') || null);
if (!mobiledocIsCompatibleWithV1(mobiledoc)) {
version = 2;
}
// CASE: revert: all new editor posts to the old editor format
if (mobiledoc && post.get('html')) {
html = converters.mobiledocConverter.render(mobiledoc, version);
}
return models.Post.edit({
html: html || post.get('html')
}, _.merge({id: post.id}, localOptions));
}, {concurrency: 100});
})
.then(() => {
common.logging.info(message4);
});
};

View file

@ -0,0 +1,42 @@
const _ = require('lodash'),
Promise = require('bluebird'),
common = require('../../../../lib/common'),
models = require('../../../../models'),
message1 = 'Removing `koenigEditor` from labs.',
message2 = 'Removed `koenigEditor` from labs.',
message3 = 'Rollback: Please re-enable König Beta if required. We can\'t rollback this change.';
module.exports.config = {
transaction: true
};
module.exports.up = (options) => {
let localOptions = _.merge({
context: {internal: true}
}, options);
return models.Settings.findOne({key: 'labs'}, localOptions)
.then(function (settingsModel) {
if (!settingsModel) {
common.logging.warn('Labs field does not exist.');
return;
}
const labsValue = JSON.parse(settingsModel.get('value'));
delete labsValue.koenigEditor;
common.logging.info(message1);
return models.Settings.edit({
key: 'labs',
value: JSON.stringify(labsValue)
}, localOptions);
})
.then(() => {
common.logging.info(message2);
});
};
module.exports.down = () => {
common.logging.warn(message3);
return Promise.resolve();
};

View file

@ -4,11 +4,13 @@ module.exports = {
render: function (opts) {
let converters = require('../converters');
let payload = opts.payload;
let version = opts.options.version;
let version = opts.options && opts.options.version || 2;
// convert markdown to HTML ready for insertion into dom
let html = converters.markdownConverter.render(payload.markdown || '');
// Ghost 1.0's markdown-only renderer wrapped cards
/**
* @deprecated Ghost 1.0's markdown-only renderer wrapped cards
*/
if (version === 1) {
html = `<div class="kg-card-markdown">${html}</div>`;
}

View file

@ -88,14 +88,15 @@ class DomModifier {
}
module.exports = {
// version 1 === Ghost 1.0 markdown-only mobiledoc
// version 2 === Ghost 2.0 full mobiledoc
render(mobiledoc, version) {
version = version || 1;
/**
* @deprecated: version 1 === Ghost 1.0 markdown-only mobiledoc
* We keep the version 1 logic till Ghost 3.0 to be able to rollback posts.
*
* version 2 (latest) === Ghost 2.0 full mobiledoc
*/
version = version || 2;
// pass the version through to the card renderers.
// create a new object here to avoid modifying the default options
// object because the version can change per-render until 2.0 is released
let versionedOptions = Object.assign({}, options, {
cardOptions: {version}
});
@ -116,8 +117,20 @@ module.exports = {
let modifier = new DomModifier();
modifier.modifyChildren(rendered.result);
let html = serializer.serializeChildren(rendered.result);
return serializer.serializeChildren(rendered.result);
},
return html;
blankStructure() {
return {
version: '0.3.1',
markups: [],
atoms: [],
cards: [],
sections: [
[1, 'p', [
[0, [], 0, '']
]]
]
};
}
};

View file

@ -8,7 +8,6 @@ var _ = require('lodash'),
htmlToText = require('html-to-text'),
ghostBookshelf = require('./base'),
config = require('../config'),
labs = require('../services/labs'),
converters = require('../lib/mobiledoc/converters'),
urlService = require('../services/url'),
relations = require('./relations'),
@ -198,7 +197,6 @@ Post = ghostBookshelf.Model.extend({
prevSlug = this.previous('slug'),
publishedAt = this.get('published_at'),
publishedAtHasChanged = this.hasDateChanged('published_at', {beforeWrite: true}),
mobiledoc = JSON.parse(this.get('mobiledoc') || null),
generatedFields = ['html', 'plaintext'],
tagsToSave,
ops = [];
@ -262,42 +260,21 @@ Post = ghostBookshelf.Model.extend({
ghostBookshelf.Model.prototype.onSaving.call(this, model, attr, options);
// do not allow generated fields to be overridden via the API
generatedFields.forEach((field) => {
if (this.hasChanged(field)) {
this.set(field, this.previous(field));
}
});
// render mobiledoc to HTML. Switch render version if Koenig is enabled
// or has been edited with Koenig and is no longer compatible with the
// Ghost 1.0 markdown-only renderer
// TODO: re-render all content and remove the version toggle for Ghost 2.0
if (mobiledoc) {
let version = 1;
let koenigEnabled = labs.isSet('koenigEditor') === true;
let mobiledocIsCompatibleWithV1 = function mobiledocIsCompatibleWithV1(doc) {
if (doc
&& doc.markups.length === 0
&& doc.cards.length === 1
&& doc.cards[0][0].match(/(?:card-)?markdown/)
&& doc.sections.length === 1
&& doc.sections[0].length === 2
&& doc.sections[0][0] === 10
&& doc.sections[0][1] === 0
) {
return true;
if (!options.migrating) {
generatedFields.forEach((field) => {
if (this.hasChanged(field)) {
this.set(field, this.previous(field));
}
});
}
return false;
};
if (!this.get('mobiledoc')) {
this.set('mobiledoc', JSON.stringify(converters.mobiledocConverter.blankStructure()));
}
if (koenigEnabled || !mobiledocIsCompatibleWithV1(mobiledoc)) {
version = 2;
}
let html = converters.mobiledocConverter.render(mobiledoc, version);
this.set('html', html);
// render mobiledoc to HTML
if (this.hasChanged('mobiledoc') || !this.get('html')) {
this.set('html', converters.mobiledocConverter.render(JSON.parse(this.get('mobiledoc'))));
}
if (this.hasChanged('html') || !this.get('plaintext')) {

View file

@ -1402,20 +1402,3 @@ describe('LTS', function () {
});
});
});
describe('LTS', function () {
beforeEach(testUtils.teardown);
beforeEach(testUtils.setup('roles', 'owner', 'settings'));
it('disallows importing LTS imports', function () {
const exportData = exportedLegacyBody().db[0];
return dataImporter.doImport(exportData, importOptions)
.then(function () {
"0".should.eql(1, 'LTS import should fail');
})
.catch(function (err) {
err.message.should.eql('Importing a LTS export into Ghost 2.0 is not allowed.');
});
});
});

View file

@ -4,7 +4,7 @@ var should = require('should'),
helpers = require('../../../server/helpers');
var almostOneMinute =
'<div class="kg-card-markdown"><p>Ghost has a number of different user roles for your team</p>' +
'<p>Ghost has a number of different user roles for your team</p>' +
'<h3 id="authors">Authors</h3><p>The base user level in Ghost is an author. Authors can write posts,' +
' edit their own posts, and publish their own posts. Authors are <strong>trusted</strong> users. If you ' +
'don\'t trust users to be allowed to publish their own posts, you shouldn\'t invite them to Ghost admin.</p>' +
@ -18,7 +18,7 @@ var almostOneMinute =
'The Owner can never be deleted. And in some circumstances the owner will have access to additional special settings ' +
'if applicable — for example, billing details, if using Ghost(Pro).</p><hr><p>It\'s a good idea to ask all of your' +
' users to fill out their user profiles, including bio and social links. These will populate rich structured data ' +
'for posts and generally create more opportunities for themes to fully populate their design.</p></div>';
'for posts and generally create more opportunities for themes to fully populate their design.</p>';
var almostOneAndAHalfMinute = almostOneMinute +
'<div>' +

View file

@ -11,28 +11,28 @@ describe('Helpers Utils', function () {
});
it('[success] sanitized HTML tags', function () {
var html = '<div class="kg-card-markdown"><p>This is a text example! Count me in ;)</p></div>',
var html = '<p>This is a text example! Count me in ;)</p>',
result = helperUtils.wordCount(html);
result.should.equal(8);
});
it('[success] sanitized non alpha-numeric characters', function () {
var html = '<div class="kg-card-markdown"><p>This is a text example! I love Döner. Especially number 875.</p></div>',
var html = '<p>This is a text example! I love Döner. Especially number 875.</p>',
result = helperUtils.wordCount(html);
result.should.equal(11);
});
it('[success] counted Chinese characters', function () {
var html = '<div class="kg-card-markdown"><p>我今天在家吃了好多好多好吃的,现在的我非常开心非常满足</p></div>',
var html = '<p>我今天在家吃了好多好多好吃的,现在的我非常开心非常满足</p>',
result = helperUtils.wordCount(html);
result.should.equal(26);
});
it('[success] sanitized white space correctly', function () {
var html = ' <div class="kg-card-markdown"><p> This is a text example!\n Count me in ;)</p></div> ',
var html = ' <p> This is a text example!\n Count me in ;)</p> ',
result = helperUtils.wordCount(html);
result.should.equal(8);
@ -41,7 +41,7 @@ describe('Helpers Utils', function () {
describe('Image Count', function () {
it('[success] can count images', function () {
var html = '<div class="kg-card-markdown"><p>This is a <img src="hello.png"> text example! Count me in ;)</p><img src="hello.png"></div>',
var html = '<p>This is a <img src="hello.png"> text example! Count me in ;)</p><img src="hello.png">',
result = helperUtils.imageCount(html);
result.should.equal(2);

View file

@ -4,8 +4,47 @@ const SimpleDom = require('simple-dom');
const serializer = new SimpleDom.HTMLSerializer(SimpleDom.voidMap);
describe('Markdown card', function () {
describe('version 1', function () {
describe('default', function () {
it('Markdown Card renders', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
},
payload: {
markdown: '#HEADING\r\n- list\r\n- items'
}
};
serializer.serialize(card.render(opts)).should.match('<h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n');
});
it('Accepts invalid HTML in markdown', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
},
payload: {
markdown: '#HEADING\r\n<h2>Heading 2>'
}
};
serializer.serialize(card.render(opts)).should.match('<h1 id="heading">HEADING</h1>\n<h2>Heading 2>');
});
it('Renders nothing when payload is undefined', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
},
payload: {
markdown: undefined
}
};
serializer.serialize(card.render(opts)).should.match('');
});
it('[deprecated] version 1', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
@ -20,71 +59,5 @@ describe('Markdown card', function () {
serializer.serialize(card.render(opts)).should.match('<div class="kg-card-markdown"><h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n</div>');
});
it('Accepts invalid HTML in markdown', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
},
payload: {
markdown: '#HEADING\r\n<h2>Heading 2>'
},
options: {
version: 1
}
};
serializer.serialize(card.render(opts)).should.match('<div class="kg-card-markdown"><h1 id="heading">HEADING</h1>\n<h2>Heading 2></div>');
});
});
describe('version 2', function () {
it('Markdown Card renders', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
},
payload: {
markdown: '#HEADING\r\n- list\r\n- items'
},
options: {
version: 2
}
};
serializer.serialize(card.render(opts)).should.match('<h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n');
});
it('Accepts invalid HTML in markdown', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
},
payload: {
markdown: '#HEADING\r\n<h2>Heading 2>'
},
options: {
version: 2
}
};
serializer.serialize(card.render(opts)).should.match('<h1 id="heading">HEADING</h1>\n<h2>Heading 2>');
});
it('Renders nothing when payload is undefined', function () {
let opts = {
env: {
dom: new SimpleDom.Document()
},
payload: {
markdown: undefined
},
options: {
version: 2
}
};
serializer.serialize(card.render(opts)).should.match('');
});
});
});

View file

@ -2,33 +2,7 @@ const should = require('should');
const converter = require('../../../../../server/lib/mobiledoc/converters/mobiledoc-converter');
describe('Mobiledoc converter', function () {
// version 1 === Ghost 1.0 markdown-only renderer
describe('version 1', function () {
it('renders correctly', function () {
let mobiledoc = {
version: '0.3.1',
atoms: [],
cards: [
['markdown',
{
pos: 'top',
card_name: 'markdown',
markdown: '#heading\n\n- list one\n- list two\n- list three'
}
]
],
markups: [],
sections: [
[10, 0]
]
};
converter.render(mobiledoc).should.eql('<div class="kg-card-markdown"><h1 id="heading">heading</h1>\n<ul>\n<li>list one</li>\n<li>list two</li>\n<li>list three</li>\n</ul>\n</div>');
});
});
// version 2 === Ghost 2.0 full Koenig renderer
describe('version 2', function () {
describe('default', function () {
it('renders all default cards and atoms', function () {
let mobiledoc = {
version: '0.3.1',

View file

@ -1896,14 +1896,7 @@ describe('Unit: models/post', function () {
});
});
it('uses v2 if Koenig is enabled', function () {
sandbox.stub(labs, 'isSet').callsFake(function (key) {
if (key === 'koenigEditor') {
return true;
}
return origLabs.get(key);
});
it('converts correctly', function () {
let newPost = testUtils.DataGenerator.forModel.posts[2];
return models.Post.add(
@ -1915,30 +1908,5 @@ describe('Unit: models/post', function () {
post.get('html').should.equal('<h2 id="testing">testing</h2>\n<p>mctesters</p>\n<ul>\n<li>test</li>\n<li>line</li>\n<li>items</li>\n</ul>\n');
});
});
it('uses v2 if Koenig is disabled but post is not v1 compatible', function () {
let newPost = testUtils.DataGenerator.forModel.posts[2];
newPost.mobiledoc = JSON.stringify({
version: '0.3.1',
atoms: [],
cards: [],
markups: [],
sections: [
[1, 'p', [
[0, [], 0, 'Test']
]]
]
});
return models.Post.add(
newPost,
testUtils.context.editor
).then((post) => {
should.exist(post);
post.has('html').should.equal(true);
post.get('html').should.equal('<p>Test</p>');
});
});
});
});

View file

@ -124,7 +124,7 @@ describe('RSS: Generate Feed', function () {
// item tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<div class="kg-card-markdown"><h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
xmlData.should.match(/<category><!\[CDATA\[public\]\]/);
@ -143,7 +143,7 @@ describe('RSS: Generate Feed', function () {
// special/optional tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<div class="kg-card-markdown"><h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
xmlData.should.not.match(/<dc:creator>/);
@ -161,7 +161,7 @@ describe('RSS: Generate Feed', function () {
// special/optional tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<div class="kg-card-markdown"><h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);

View file

@ -45,7 +45,7 @@ DataGenerator.Content = {
title: 'Short and Sweet',
slug: 'short-and-sweet',
mobiledoc: DataGenerator.markdownToMobiledoc('## testing\n\nmctesters\n\n- test\n- line\n- items'),
html: '<div class=\"kg-card-markdown\"><h2 id=\"testing\">testing</h2>\n<p>mctesters</p>\n<ul>\n<li>test</li>\n<li>line</li>\n<li>items</li>\n</ul>\n</div>',
html: '<h2 id=\"testing\">testing</h2>\n<p>mctesters</p>\n<ul>\n<li>test</li>\n<li>line</li>\n<li>items</li>\n</ul>\n',
plaintext: 'testing\nmctesters\n\n * test\n * line\n * items',
feature_image: 'http://placekitten.com/500/200',
meta_description: 'test stuff',

View file

@ -1090,8 +1090,8 @@
"uuid": "8c414ae2-dce6-4b0f-8ee6-5c403fa2ae86",
"title": "Setting up your own Ghost theme",
"slug": "themes",
"mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"card-markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"Creating a totally custom design for your publication\\n\\nGhost comes with a beautiful default theme called Casper, which is designed to be a clean, readable publication layout and can be easily adapted for most purposes. However, Ghost can also be completely themed to suit your needs. Rather than just giving you a few basic settings which act as a poor proxy for code, we just let you write code.\\n\\nThere are a huge range of both free and premium pre-built themes which you can get from the [Ghost Theme Marketplace](http://marketplace.ghost.org), or you can simply create your own from scratch.\\n\\n[![marketplace](https://casper.ghost.org/v1.0.0/images/marketplace.jpg)](http://marketplace.ghost.org)\\n\\n> Anyone can write a completely custom Ghost theme, with just some solid knowledge of HTML and CSS\\n\\nGhost themes are written with a templating language called handlebars, which has a bunch of dynamic helpers to insert your data into template files. Like `{{author.name}}`, for example, outputs the name of the current author.\\n\\nThe best way to learn how to write your own Ghost theme is to have a look at [the source code for Casper](https://github.com/TryGhost/Casper), which is heavily commented and should give you a sense of how everything fits together.\\n\\n- `default.hbs` is the main template file, all contexts will load inside this file unless specifically told to use a different template.\\n- `post.hbs` is the file used in the context of viewing a post.\\n- `index.hbs` is the file used in the context of viewing the home page.\\n- and so on\\n\\nWe've got [full and extensive theme documentation](http://themes.ghost.org/v1.23.0/docs/about) which outlines every template file, context and helper that you can use.\\n\\nIf you want to chat with other people making Ghost themes to get any advice or help, there's also a **themes** section on our [public Ghost forum](https://forum.ghost.org/c/themes).\"}]],\"sections\":[[10,0]]}",
"html": "<div class=\"kg-card-markdown\"><p>Creating a totally custom design for your publication</p>\n<p>Ghost comes with a beautiful default theme called Casper, which is designed to be a clean, readable publication layout and can be easily adapted for most purposes. However, Ghost can also be completely themed to suit your needs. Rather than just giving you a few basic settings which act as a poor proxy for code, we just let you write code.</p>\n<p>There are a huge range of both free and premium pre-built themes which you can get from the <a href=\"http://marketplace.ghost.org\">Ghost Theme Marketplace</a>, or you can simply create your own from scratch.</p>\n<p><a href=\"http://marketplace.ghost.org\"><img src=\"https://casper.ghost.org/v1.0.0/images/marketplace.jpg\" alt=\"marketplace\"></a></p>\n<blockquote>\n<p>Anyone can write a completely custom Ghost theme, with just some solid knowledge of HTML and CSS</p>\n</blockquote>\n<p>Ghost themes are written with a templating language called handlebars, which has a bunch of dynamic helpers to insert your data into template files. Like <code>{{author.name}}</code>, for example, outputs the name of the current author.</p>\n<p>The best way to learn how to write your own Ghost theme is to have a look at <a href=\"https://github.com/TryGhost/Casper\">the source code for Casper</a>, which is heavily commented and should give you a sense of how everything fits together.</p>\n<ul>\n<li><code>default.hbs</code> is the main template file, all contexts will load inside this file unless specifically told to use a different template.</li>\n<li><code>post.hbs</code> is the file used in the context of viewing a post.</li>\n<li><code>index.hbs</code> is the file used in the context of viewing the home page.</li>\n<li>and so on</li>\n</ul>\n<p>We've got <a href=\"http://themes.ghost.org/v1.23.0/docs/about\">full and extensive theme documentation</a> which outlines every template file, context and helper that you can use.</p>\n<p>If you want to chat with other people making Ghost themes to get any advice or help, there's also a <strong>themes</strong> section on our <a href=\"https://forum.ghost.org/c/themes\">public Ghost forum</a>.</p>\n</div>",
"mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"card-markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"Creating a totally custom design for your publication\\n\\nGhost comes with a beautiful default theme called Casper, which is designed to be a clean, readable publication layout and can be easily adapted for most purposes. However, Ghost can also be completely themed to suit your needs. Rather than just giving you a few basic settings which act as a poor proxy for code, we just let you write code.\\n\\nThere are a huge range of both free and premium pre-built themes which you can get from the [Ghost Theme Marketplace](http://marketplace.ghost.org), or you can simply create your own from scratch.\\n\\n[![marketplace](https://casper.ghost.org/v1.0.0/images/marketplace.jpg)](http://marketplace.ghost.org)\\n\\n> Anyone can write a completely custom Ghost theme, with just some solid knowledge of HTML and CSS\\n\\nGhost themes are written with a templating language called handlebars, which has a bunch of dynamic helpers to insert your data into template files. Like `{{author.name}}`, for example, outputs the name of the current author.\\n\\nThe best way to learn how to write your own Ghost theme is to have a look at [the source code for Casper](https://github.com/TryGhost/Casper), which is heavily commented and should give you a sense of how everything fits together.\\n\\n- `default.hbs` is the main template file, all contexts will load inside this file unless specifically told to use a different template.\\n- `post.hbs` is the file used in the context of viewing a post.\\n- `index.hbs` is the file used in the context of viewing the home page.\\n- and so on\\n\\nWe've got [full and extensive theme documentation](http://themes.ghost.org/docs/about) which outlines every template file, context and helper that you can use.\\n\\nIf you want to chat with other people making Ghost themes to get any advice or help, there's also a **themes** section on our [public Ghost forum](https://forum.ghost.org/c/themes).\"}]],\"sections\":[[10,0]]}",
"html": "<p>Creating a totally custom design for your publication</p>\n<p>Ghost comes with a beautiful default theme called Casper, which is designed to be a clean, readable publication layout and can be easily adapted for most purposes. However, Ghost can also be completely themed to suit your needs. Rather than just giving you a few basic settings which act as a poor proxy for code, we just let you write code.</p>\n<p>There are a huge range of both free and premium pre-built themes which you can get from the <a href=\"http://marketplace.ghost.org\">Ghost Theme Marketplace</a>, or you can simply create your own from scratch.</p>\n<p><a href=\"http://marketplace.ghost.org\"><img src=\"https://casper.ghost.org/v1.0.0/images/marketplace.jpg\" alt=\"marketplace\"></a></p>\n<blockquote>\n<p>Anyone can write a completely custom Ghost theme, with just some solid knowledge of HTML and CSS</p>\n</blockquote>\n<p>Ghost themes are written with a templating language called handlebars, which has a bunch of dynamic helpers to insert your data into template files. Like <code>{{author.name}}</code>, for example, outputs the name of the current author.</p>\n<p>The best way to learn how to write your own Ghost theme is to have a look at <a href=\"https://github.com/TryGhost/Casper\">the source code for Casper</a>, which is heavily commented and should give you a sense of how everything fits together.</p>\n<ul>\n<li><code>default.hbs</code> is the main template file, all contexts will load inside this file unless specifically told to use a different template.</li>\n<li><code>post.hbs</code> is the file used in the context of viewing a post.</li>\n<li><code>index.hbs</code> is the file used in the context of viewing the home page.</li>\n<li>and so on</li>\n</ul>\n<p>We've got <a href=\"http://themes.ghost.org/docs/about\">full and extensive theme documentation</a> which outlines every template file, context and helper that you can use.</p>\n<p>If you want to chat with other people making Ghost themes to get any advice or help, there's also a <strong>themes</strong> section on our <a href=\"https://forum.ghost.org/c/themes\">public Ghost forum</a>.</p>",
"amp": "1",
"plaintext": "Creating a totally custom design for your publication\n\nGhost comes with a beautiful default theme called Casper, which is designed to\nbe a clean, readable publication layout and can be easily adapted for most\npurposes. However, Ghost can also be completely themed to suit your needs.\nRather than just giving you a few basic settings which act as a poor proxy for\ncode, we just let you write code.\n\nThere are a huge range of both free and premium pre-built themes which you can\nget from the Ghost Theme Marketplace [http://marketplace.ghost.org], or you can\nsimply create your own from scratch.\n\n [http://marketplace.ghost.org]\n\nAnyone can write a completely custom Ghost theme, with just some solid knowledge\nof HTML and CSS\n\nGhost themes are written with a templating language called handlebars, which has\na bunch of dynamic helpers to insert your data into template files. Like \n{{author.name}}, for example, outputs the name of the current author.\n\nThe best way to learn how to write your own Ghost theme is to have a look at \nthe\nsource code for Casper [https://github.com/TryGhost/Casper], which is heavily\ncommented and should give you a sense of how everything fits together.\n\n * default.hbs is the main template file, all contexts will load inside this\n file unless specifically told to use a different template.\n * post.hbs is the file used in the context of viewing a post.\n * index.hbs is the file used in the context of viewing the home page.\n * and so on\n\nWe've got full and extensive theme documentation\n[http://themes.ghost.org/docs/about] which outlines every template file,\ncontext and helper that you can use.\n\nIf you want to chat with other people making Ghost themes to get any advice or\nhelp, there's also a themes channel on our public Ghost forum\n[https://slack.ghost.org].",
"feature_image": "https://casper.ghost.org/v1.0.0/images/design.jpg",
@ -1125,7 +1125,7 @@
"title": "Advanced Markdown tips",
"slug": "advanced-markdown",
"mobiledoc": "{\"version\":\"0.3.1\",\"markups\":[],\"atoms\":[],\"cards\":[[\"card-markdown\",{\"cardName\":\"card-markdown\",\"markdown\":\"There are lots of powerful things you can do with the Ghost editor\\n\\nIf you've gotten pretty comfortable with [all the basics](/the-editor/) of writing in Ghost, then you may enjoy some more advanced tips about the types of things you can do with Markdown!\\n\\nAs with the last post about the editor, you'll want to be actually editing this post as you read it so that you can see all the Markdown code we're using.\\n\\n\\n## Special formatting\\n\\nAs well as bold and italics, you can also use some other special formatting in Markdown when the need arises, for example:\\n\\n+ ~~strike through~~\\n+ ==highlight==\\n+ \\\\*escaped characters\\\\*\\n\\n\\n## Writing code blocks\\n\\nThere are two types of code elements which can be inserted in Markdown, the first is inline, and the other is block. Inline code is formatted by wrapping any word or words in back-ticks, `like this`. Larger snippets of code can be displayed across multiple lines using triple back ticks:\\n\\n```\\n.my-link {\\n text-decoration: underline;\\n}\\n```\\n\\nIf you want to get really fancy, you can even add syntax highlighting using [Prism.js](http://prismjs.com/).\\n\\n\\n## Full bleed images\\n\\nOne neat trick which you can use in Markdown to distinguish between different types of images is to add a `#hash` value to the end of the source URL, and then target images containing the hash with special styling. For example:\\n\\n![walking](https://casper.ghost.org/v1.0.0/images/walking.jpg#full)\\n\\nwhich is styled with...\\n\\n```\\nimg[src$=\\\"#full\\\"] {\\n max-width: 100vw;\\n}\\n```\\n\\nThis creates full-bleed images in the Casper theme, which stretch beyond their usual boundaries right up to the edge of the window. Every theme handles these types of things slightly differently, but it's a great trick to play with if you want to have a variety of image sizes and styles.\\n\\n\\n## Reference lists\\n\\n**The quick brown [fox][1], jumped over the lazy [dog][2].**\\n\\n[1]: https://en.wikipedia.org/wiki/Fox \\\"Wikipedia: Fox\\\"\\n[2]: https://en.wikipedia.org/wiki/Dog \\\"Wikipedia: Dog\\\"\\n\\nAnother way to insert links in markdown is using reference lists. You might want to use this style of linking to cite reference material in a Wikipedia-style. All of the links are listed at the end of the document, so you can maintain full separation between content and its source or reference.\\n\\n\\n## Creating footnotes\\n\\nThe quick brown fox[^1] jumped over the lazy dog[^2].\\n\\n[^1]: Foxes are red\\n[^2]: Dogs are usually not red\\n\\nFootnotes are a great way to add additional contextual details when appropriate. Ghost will automatically add footnote content to the very end of your post.\\n\\n\\n## Full HTML\\n\\nPerhaps the best part of Markdown is that you're never limited to just Markdown. You can write HTML directly in the Ghost editor and it will just work as HTML usually does. No limits! Here's a standard YouTube embed code as an example:\\n\\n<iframe width=\\\"560\\\" height=\\\"315\\\" src=\\\"https://www.youtube.com/embed/Cniqsc9QfDo?rel=0&amp;showinfo=0\\\" frameborder=\\\"0\\\" allowfullscreen></iframe>\\n\"}]],\"sections\":[[10,0]]}",
"html": "<div class=\"kg-card-markdown\"><p>There are lots of powerful things you can do with the Ghost editor</p>\n<p>If you've gotten pretty comfortable with <a href=\"/the-editor/\">all the basics</a> of writing in Ghost, then you may enjoy some more advanced tips about the types of things you can do with Markdown!</p>\n<p>As with the last post about the editor, you'll want to be actually editing this post as you read it so that you can see all the Markdown code we're using.</p>\n<h2 id=\"specialformatting\">Special formatting</h2>\n<p>As well as bold and italics, you can also use some other special formatting in Markdown when the need arises, for example:</p>\n<ul>\n<li><s>strike through</s></li>\n<li><mark>highlight</mark></li>\n<li>*escaped characters*</li>\n</ul>\n<h2 id=\"writingcodeblocks\">Writing code blocks</h2>\n<p>There are two types of code elements which can be inserted in Markdown, the first is inline, and the other is block. Inline code is formatted by wrapping any word or words in back-ticks, <code>like this</code>. Larger snippets of code can be displayed across multiple lines using triple back ticks:</p>\n<pre><code>.my-link {\n text-decoration: underline;\n}\n</code></pre>\n<p>If you want to get really fancy, you can even add syntax highlighting using <a href=\"http://prismjs.com/\">Prism.js</a>.</p>\n<h2 id=\"fullbleedimages\">Full bleed images</h2>\n<p>One neat trick which you can use in Markdown to distinguish between different types of images is to add a <code>#hash</code> value to the end of the source URL, and then target images containing the hash with special styling. For example:</p>\n<p><img src=\"https://casper.ghost.org/v1.0.0/images/walking.jpg#full\" alt=\"walking\"></p>\n<p>which is styled with...</p>\n<pre><code>img[src$=&quot;#full&quot;] {\n max-width: 100vw;\n}\n</code></pre>\n<p>This creates full-bleed images in the Casper theme, which stretch beyond their usual boundaries right up to the edge of the window. Every theme handles these types of things slightly differently, but it's a great trick to play with if you want to have a variety of image sizes and styles.</p>\n<h2 id=\"referencelists\">Reference lists</h2>\n<p><strong>The quick brown <a href=\"https://en.wikipedia.org/wiki/Fox\" title=\"Wikipedia: Fox\">fox</a>, jumped over the lazy <a href=\"https://en.wikipedia.org/wiki/Dog\" title=\"Wikipedia: Dog\">dog</a>.</strong></p>\n<p>Another way to insert links in markdown is using reference lists. You might want to use this style of linking to cite reference material in a Wikipedia-style. All of the links are listed at the end of the document, so you can maintain full separation between content and its source or reference.</p>\n<h2 id=\"creatingfootnotes\">Creating footnotes</h2>\n<p>The quick brown fox<sup class=\"footnote-ref\"><a href=\"#fn1\" id=\"fnref1\">[1]</a></sup> jumped over the lazy dog<sup class=\"footnote-ref\"><a href=\"#fn2\" id=\"fnref2\">[2]</a></sup>.</p>\n<p>Footnotes are a great way to add additional contextual details when appropriate. Ghost will automatically add footnote content to the very end of your post.</p>\n<h2 id=\"fullhtml\">Full HTML</h2>\n<p>Perhaps the best part of Markdown is that you're never limited to just Markdown. You can write HTML directly in the Ghost editor and it will just work as HTML usually does. No limits! Here's a standard YouTube embed code as an example:</p>\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/Cniqsc9QfDo?rel=0&amp;showinfo=0\" frameborder=\"0\" allowfullscreen></iframe>\n<hr class=\"footnotes-sep\">\n<section class=\"footnotes\">\n<ol class=\"footnotes-list\">\n<li id=\"fn1\" class=\"footnote-item\"><p>Foxes are red <a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n<li id=\"fn2\" class=\"footnote-item\"><p>Dogs are usually not red <a href=\"#fnref2\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n</ol>\n</section>\n</div>",
"html": "<p>There are lots of powerful things you can do with the Ghost editor</p>\n<p>If you've gotten pretty comfortable with <a href=\"/the-editor/\">all the basics</a> of writing in Ghost, then you may enjoy some more advanced tips about the types of things you can do with Markdown!</p>\n<p>As with the last post about the editor, you'll want to be actually editing this post as you read it so that you can see all the Markdown code we're using.</p>\n<h2 id=\"specialformatting\">Special formatting</h2>\n<p>As well as bold and italics, you can also use some other special formatting in Markdown when the need arises, for example:</p>\n<ul>\n<li><s>strike through</s></li>\n<li><mark>highlight</mark></li>\n<li>*escaped characters*</li>\n</ul>\n<h2 id=\"writingcodeblocks\">Writing code blocks</h2>\n<p>There are two types of code elements which can be inserted in Markdown, the first is inline, and the other is block. Inline code is formatted by wrapping any word or words in back-ticks, <code>like this</code>. Larger snippets of code can be displayed across multiple lines using triple back ticks:</p>\n<pre><code>.my-link {\n text-decoration: underline;\n}\n</code></pre>\n<p>If you want to get really fancy, you can even add syntax highlighting using <a href=\"http://prismjs.com/\">Prism.js</a>.</p>\n<h2 id=\"fullbleedimages\">Full bleed images</h2>\n<p>One neat trick which you can use in Markdown to distinguish between different types of images is to add a <code>#hash</code> value to the end of the source URL, and then target images containing the hash with special styling. For example:</p>\n<p><img src=\"https://casper.ghost.org/v1.0.0/images/walking.jpg#full\" alt=\"walking\"></p>\n<p>which is styled with...</p>\n<pre><code>img[src$=&quot;#full&quot;] {\n max-width: 100vw;\n}\n</code></pre>\n<p>This creates full-bleed images in the Casper theme, which stretch beyond their usual boundaries right up to the edge of the window. Every theme handles these types of things slightly differently, but it's a great trick to play with if you want to have a variety of image sizes and styles.</p>\n<h2 id=\"referencelists\">Reference lists</h2>\n<p><strong>The quick brown <a href=\"https://en.wikipedia.org/wiki/Fox\" title=\"Wikipedia: Fox\">fox</a>, jumped over the lazy <a href=\"https://en.wikipedia.org/wiki/Dog\" title=\"Wikipedia: Dog\">dog</a>.</strong></p>\n<p>Another way to insert links in markdown is using reference lists. You might want to use this style of linking to cite reference material in a Wikipedia-style. All of the links are listed at the end of the document, so you can maintain full separation between content and its source or reference.</p>\n<h2 id=\"creatingfootnotes\">Creating footnotes</h2>\n<p>The quick brown fox<sup class=\"footnote-ref\"><a href=\"#fn1\" id=\"fnref1\">[1]</a></sup> jumped over the lazy dog<sup class=\"footnote-ref\"><a href=\"#fn2\" id=\"fnref2\">[2]</a></sup>.</p>\n<p>Footnotes are a great way to add additional contextual details when appropriate. Ghost will automatically add footnote content to the very end of your post.</p>\n<h2 id=\"fullhtml\">Full HTML</h2>\n<p>Perhaps the best part of Markdown is that you're never limited to just Markdown. You can write HTML directly in the Ghost editor and it will just work as HTML usually does. No limits! Here's a standard YouTube embed code as an example:</p>\n<iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/Cniqsc9QfDo?rel=0&amp;showinfo=0\" frameborder=\"0\" allowfullscreen></iframe>\n<hr class=\"footnotes-sep\">\n<section class=\"footnotes\">\n<ol class=\"footnotes-list\">\n<li id=\"fn1\" class=\"footnote-item\"><p>Foxes are red <a href=\"#fnref1\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n<li id=\"fn2\" class=\"footnote-item\"><p>Dogs are usually not red <a href=\"#fnref2\" class=\"footnote-backref\">↩︎</a></p>\n</li>\n</ol>\n</section>",
"amp": null,
"plaintext": "There are lots of powerful things you can do with the Ghost editor\n\nIf you've gotten pretty comfortable with all the basics [/the-editor/] of\nwriting in Ghost, then you may enjoy some more advanced tips about the types of\nthings you can do with Markdown!\n\nAs with the last post about the editor, you'll want to be actually editing this\npost as you read it so that you can see all the Markdown code we're using.\n\nSpecial formatting\nAs well as bold and italics, you can also use some other special formatting in\nMarkdown when the need arises, for example:\n\n * strike through\n * highlight\n * *escaped characters*\n\nWriting code blocks\nThere are two types of code elements which can be inserted in Markdown, the\nfirst is inline, and the other is block. Inline code is formatted by wrapping\nany word or words in back-ticks, like this. Larger snippets of code can be\ndisplayed across multiple lines using triple back ticks:\n\n.my-link {\n text-decoration: underline;\n}\n\n\nIf you want to get really fancy, you can even add syntax highlighting using \nPrism.js [http://prismjs.com/].\n\nFull bleed images\nOne neat trick which you can use in Markdown to distinguish between different\ntypes of images is to add a #hash value to the end of the source URL, and then\ntarget images containing the hash with special styling. For example:\n\n\n\nwhich is styled with...\n\nimg[src$=\"#full\"] {\n max-width: 100vw;\n}\n\n\nThis creates full-bleed images in the Casper theme, which stretch beyond their\nusual boundaries right up to the edge of the window. Every theme handles these\ntypes of things slightly differently, but it's a great trick to play with if you\nwant to have a variety of image sizes and styles.\n\nReference lists\nThe quick brown fox [https://en.wikipedia.org/wiki/Fox], jumped over the lazy \ndog [https://en.wikipedia.org/wiki/Dog].\n\nAnother way to insert links in markdown is using reference lists. You might want\nto use this style of linking to cite reference material in a Wikipedia-style.\nAll of the links are listed at the end of the document, so you can maintain full\nseparation between content and its source or reference.\n\nCreating footnotes\nThe quick brown fox[1] jumped over the lazy dog[2].\n\nFootnotes are a great way to add additional contextual details when appropriate.\nGhost will automatically add footnote content to the very end of your post.\n\nFull HTML\nPerhaps the best part of Markdown is that you're never limited to just Markdown.\nYou can write HTML directly in the Ghost editor and it will just work as HTML\nusually does. No limits! Here's a standard YouTube embed code as an example:\n\n\n--------------------------------------------------------------------------------\n\n 1. Foxes are red ↩︎\n \n \n 2. Dogs are usually not red ↩︎",
"feature_image": "https://casper.ghost.org/v1.0.0/images/advanced.jpg",