From 41677a5209385917e1edf8b674c4f3f6f6815a3c Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Thu, 25 Jun 2026 16:30:15 +0100 Subject: [PATCH] Removed mobiledoc rendering and HTML-to-mobiledoc conversion no ref - Ghost moved to Lexical years ago but kept two legacy mobiledoc paths alive: converting HTML to mobiledoc (via ?source=html and on import) and rendering stored mobiledoc to HTML. These only ran for rare edge cases yet forced the mobiledoc-kit based dependencies to stay installed. - HTML now always converts to Lexical, and any content still stored as mobiledoc is converted to Lexical (via @tryghost/kg-converters) before rendering, leaving a single rendering path. - This drops @tryghost/kg-mobiledoc-html-renderer, @tryghost/html-to-mobiledoc and @tryghost/kg-default-atoms, removing mobiledoc-kit from ghost/core's dependency tree. - Mobiledoc is still stored for legacy posts; only the conversion and rendering paths are removed, so existing mobiledoc content keeps working. --- .../utils/serializers/input/pages.js | 21 - .../utils/serializers/input/posts.js | 21 - .../importer/importers/data/posts-importer.js | 15 +- .../2022-05-21-00-00-regenerate-posts-html.js | 5 +- ghost/core/core/server/lib/mobiledoc.js | 111 -- ghost/core/core/server/models/post.js | 39 +- .../services/email-service/email-renderer.js | 29 +- .../email-service/email-service-wrapper.js | 2 - ghost/core/package.json | 3 - .../__snapshots__/activity-feed.test.js.snap | 2 +- .../__snapshots__/email-previews.test.js.snap | 4 +- .../admin/__snapshots__/members.test.js.snap | 2 +- .../admin/__snapshots__/posts.test.js.snap | 42 +- .../test/e2e-api/admin/pages-legacy.test.js | 5 +- .../test/e2e-api/admin/posts-legacy.test.js | 27 +- ghost/core/test/e2e-api/admin/posts.test.js | 13 +- .../content/__snapshots__/pages.test.js.snap | 10 +- .../content/__snapshots__/posts.test.js.snap | 992 ++++++++++++------ .../__snapshots__/search-index.test.js.snap | 2 +- ghost/core/test/e2e-api/content/posts.test.js | 2 +- .../__snapshots__/posts.test.js.snap | 8 +- .../core/test/integration/importer/v1.test.js | 50 +- .../core/test/integration/importer/v2.test.js | 22 +- .../email-service/batch-sending.test.js | 3 +- .../core/test/legacy/api/admin/pages.test.js | 4 +- .../test/legacy/models/model-posts.test.js | 137 +-- .../utils/serializers/input/pages.test.js | 6 +- .../utils/serializers/input/posts.test.js | 12 +- .../test/unit/server/lib/mobiledoc.test.js | 399 ------- ghost/core/test/utils/batch-email-utils.js | 6 +- ghost/core/test/utils/fixture-utils.js | 23 + pnpm-lock.yaml | 123 +-- 32 files changed, 919 insertions(+), 1221 deletions(-) delete mode 100644 ghost/core/test/unit/server/lib/mobiledoc.test.js diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js index 5203549195f..7f8e1dceb66 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js @@ -6,14 +6,12 @@ const tpl = require('@tryghost/tpl'); const url = require('./utils/url'); const slugFilterOrder = require('./utils/slug-filter-order'); const localUtils = require('../../index'); -const mobiledoc = require('../../../../../lib/mobiledoc'); const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta; const postsSchema = require('../../../../../data/schema').tables.posts; const clean = require('./utils/clean'); const lexical = require('../../../../../lib/lexical'); const messages = { - failedHtmlToMobiledoc: 'Failed to convert HTML to Mobiledoc', failedHtmlToLexical: 'Failed to convert HTML to Lexical' }; @@ -178,25 +176,6 @@ module.exports = { const html = frame.data.pages[0].html; if (frame.options.source === 'html' && !_.isEmpty(html)) { - if (process.env.CI) { - console.time('htmlToMobiledocConverter (page)'); // eslint-disable-line no-console - } - - try { - frame.data.pages[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html)); - } catch (err) { - throw new ValidationError({ - message: tpl(messages.failedHtmlToMobiledoc), - err - }); - } - - if (process.env.CI) { - console.timeEnd('htmlToMobiledocConverter (page)'); // eslint-disable-line no-console - } - - // normally we don't allow both mobiledoc+lexical but the model layer will remove lexical - // if mobiledoc is already present to avoid migrating formats outside of an explicit conversion if (process.env.CI) { console.time('htmlToLexicalConverter (page)'); // eslint-disable-line no-console } diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js index facb39264ab..6a7032eec41 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/posts.js @@ -6,14 +6,12 @@ const tpl = require('@tryghost/tpl'); const url = require('./utils/url'); const slugFilterOrder = require('./utils/slug-filter-order'); const localUtils = require('../../index'); -const mobiledoc = require('../../../../../lib/mobiledoc'); const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta; const postsSchema = require('../../../../../data/schema').tables.posts; const clean = require('./utils/clean'); const lexical = require('../../../../../lib/lexical'); const messages = { - failedHtmlToMobiledoc: 'Failed to convert HTML to Mobiledoc', failedHtmlToLexical: 'Failed to convert HTML to Lexical' }; @@ -222,25 +220,6 @@ module.exports = { const html = frame.data.posts[0].html; if (frame.options.source === 'html' && !_.isEmpty(html)) { - if (process.env.CI) { - console.time('htmlToMobiledocConverter (post)'); // eslint-disable-line no-console - } - - try { - frame.data.posts[0].mobiledoc = JSON.stringify(mobiledoc.htmlToMobiledocConverter(html)); - } catch (err) { - throw new ValidationError({ - message: tpl(messages.failedHtmlToMobiledoc), - err - }); - } - - if (process.env.CI) { - console.timeEnd('htmlToMobiledocConverter (post)'); // eslint-disable-line no-console - } - - // normally we don't allow both mobiledoc+lexical but the model layer will remove lexical - // if mobiledoc is already present to avoid migrating formats outside of an explicit conversion if (process.env.CI) { console.time('htmlToLexicalConverter (post)'); // eslint-disable-line no-console } diff --git a/ghost/core/core/server/data/importer/importers/data/posts-importer.js b/ghost/core/core/server/data/importer/importers/data/posts-importer.js index 59b9ba7f65f..0340cb8b6f5 100644 --- a/ghost/core/core/server/data/importer/importers/data/posts-importer.js +++ b/ghost/core/core/server/data/importer/importers/data/posts-importer.js @@ -3,6 +3,7 @@ const _ = require('lodash'); const crypto = require('crypto'); const BaseImporter = require('./base'); const mobiledocLib = require('../../../../lib/mobiledoc'); +const lexicalLib = require('../../../../lib/lexical'); const validator = require('@tryghost/validator'); const postsMetaSchema = require('../../../schema').tables.posts_meta; const metaAttrs = _.keys(_.omit(postsMetaSchema, ['id'])); @@ -244,8 +245,7 @@ class PostsImporter extends BaseImporter { mobiledoc = JSON.parse(model.mobiledoc); if (!mobiledoc.cards || !_.isArray(mobiledoc.cards)) { - model.mobiledoc = mobiledocLib.blankDocument; - mobiledoc = model.mobiledoc; + mobiledoc = mobiledocLib.blankDocument; } } catch (err) { mobiledoc = mobiledocLib.blankDocument; @@ -270,11 +270,16 @@ class PostsImporter extends BaseImporter { } }); + // keep the cleaned-up mobiledoc and let the model convert it to lexical and + // render the html on save - this exercises the same mobiledoc -> lexical + // import path that real legacy exports go through model.mobiledoc = JSON.stringify(mobiledoc); - model.html = mobiledocLib.render(JSON.parse(model.mobiledoc)); + delete model.html; } else if (model.html && !model.lexical) { - model.mobiledoc = JSON.stringify(mobiledocLib.htmlToMobiledocConverter(model.html)); - model.html = mobiledocLib.render(JSON.parse(model.mobiledoc)); + // html-only imports have no mobiledoc to convert, so turn the html into lexical + model.lexical = JSON.stringify(lexicalLib.htmlToLexicalConverter(model.html)); + delete model.mobiledoc; + delete model.html; } this.sanitizePostsMeta(model); diff --git a/ghost/core/core/server/data/migrations/versions/5.0/2022-05-21-00-00-regenerate-posts-html.js b/ghost/core/core/server/data/migrations/versions/5.0/2022-05-21-00-00-regenerate-posts-html.js index 547f1f98718..50214203040 100644 --- a/ghost/core/core/server/data/migrations/versions/5.0/2022-05-21-00-00-regenerate-posts-html.js +++ b/ghost/core/core/server/data/migrations/versions/5.0/2022-05-21-00-00-regenerate-posts-html.js @@ -1,6 +1,7 @@ const logging = require('@tryghost/logging'); const {createIrreversibleMigration} = require('../../utils'); -const mobiledocLib = require('../../../../lib/mobiledoc'); +const lexicalLib = require('../../../../lib/lexical'); +const {mobiledocToLexical} = require('@tryghost/kg-converters'); const htmlToPlaintext = require('@tryghost/html-to-plaintext'); module.exports = createIrreversibleMigration(async (knex) => { @@ -43,7 +44,7 @@ module.exports = createIrreversibleMigration(async (knex) => { continue; } - const html = mobiledocLib.render(mobiledoc); + const html = await lexicalLib.render(mobiledocToLexical(post.mobiledoc)); const updatedAttrs = { html diff --git a/ghost/core/core/server/lib/mobiledoc.js b/ghost/core/core/server/lib/mobiledoc.js index 4110f914078..5476f465577 100644 --- a/ghost/core/core/server/lib/mobiledoc.js +++ b/ghost/core/core/server/lib/mobiledoc.js @@ -1,13 +1,9 @@ const path = require('path'); -const errors = require('@tryghost/errors'); -const logging = require('@tryghost/logging'); const config = require('../../shared/config'); const storage = require('../adapters/storage'); -const storageUtils = require('../adapters/storage/utils'); let cardFactory; let cards; -let mobiledocHtmlRenderer; module.exports = { get blankDocument() { @@ -53,116 +49,9 @@ module.exports = { return cards; }, - get atoms() { - return require('@tryghost/kg-default-atoms').atoms; - }, - - get mobiledocHtmlRenderer() { - if (!mobiledocHtmlRenderer) { - const {MobiledocHtmlRenderer} = require('@tryghost/kg-mobiledoc-html-renderer'); - - mobiledocHtmlRenderer = new MobiledocHtmlRenderer({ - cards: this.cards, - atoms: this.atoms, - unknownCardHandler(args) { - logging.error(new errors.InternalServerError({ - message: 'Mobiledoc card \'' + args.env.name + '\' not found.' - })); - } - }); - } - - return mobiledocHtmlRenderer; - }, - - render(mobiledoc, options) { - return this.mobiledocHtmlRenderer.render(mobiledoc, options); - }, - - get htmlToMobiledocConverter() { - try { - if (process.env.CI) { - console.time('require @tryghost/html-to-mobiledoc'); // eslint-disable-line no-console - } - - const {htmlToMobiledoc} = require('@tryghost/html-to-mobiledoc'); - - if (process.env.CI) { - console.timeEnd('require @tryghost/html-to-mobiledoc'); // eslint-disable-line no-console - } - - return htmlToMobiledoc; - } catch (err) { - return () => { - throw new errors.InternalServerError({ - message: 'Unable to convert from source HTML to Mobiledoc', - context: 'The html-to-mobiledoc package was not installed', - help: 'Please review any errors from the install process by checking the Ghost logs', - code: 'HTML_TO_MOBILEDOC_INSTALLATION', - err: err - }); - }; - } - }, - - // used when force-rerendering post content to ensure that old image card - // payloads contain width/height values to be used when generating srcsets - populateImageSizes: async function (mobiledocJson) { - // do not require image-size until it's requested to avoid circular dependencies - // shared/url-utils > server/lib/mobiledoc > server/lib/image/image-size > server/adapters/storage/utils - const {imageSize} = require('./image'); - - async function getUnsplashSize(url) { - const parsedUrl = new URL(url); - parsedUrl.searchParams.delete('w'); - parsedUrl.searchParams.delete('fit'); - parsedUrl.searchParams.delete('crop'); - parsedUrl.searchParams.delete('dpr'); - - return await imageSize.getImageSizeFromUrl(parsedUrl.href); - } - - const mobiledoc = JSON.parse(mobiledocJson); - - const sizePromises = mobiledoc.cards.map(async (card) => { - const [cardName, payload] = card; - - const needsFilling = cardName === 'image' && payload && payload.src && (!payload.width || !payload.height); - if (!needsFilling) { - return; - } - - const isUnsplash = payload.src.match(/images\.unsplash\.com/); - const isRelativeImagePath = /^\/content\/images\//.test(payload.src); - try { - let size; - if (isUnsplash) { - size = await getUnsplashSize(payload.src); - } else if (isRelativeImagePath || storageUtils.isLocalImage(payload.src)) { - size = await imageSize.getOriginalImageSizeFromStorageUrl(payload.src); - } else if (storageUtils.isInternalImage(payload.src)) { - size = await imageSize.getImageSizeFromUrl(payload.src); - } - - if (size && size.width && size.height) { - payload.width = size.width; - payload.height = size.height; - } - } catch (e) { - // TODO: use debug instead? - logging.error(e); - } - }); - - await Promise.all(sizePromises); - - return JSON.stringify(mobiledoc); - }, - // allow config changes to be picked up - useful in tests reload() { cardFactory = null; cards = null; - mobiledocHtmlRenderer = null; } }; diff --git a/ghost/core/core/server/models/post.js b/ghost/core/core/server/models/post.js index fd8bb204a80..7d5064fc110 100644 --- a/ghost/core/core/server/models/post.js +++ b/ghost/core/core/server/models/post.js @@ -556,15 +556,22 @@ Post = ghostBookshelf.Model.extend({ let tagsToSave; const ops = []; - // normally we don't allow both mobiledoc & lexical through at the API level but there's - // an exception for ?source=html which always sets both when the lexical editor is enabled. - // That's necessary because at the input serializer layer we don't have access to the - // actual model to check if this would result in a change of format - + // We don't allow both mobiledoc & lexical to be stored at once. A single save can + // still momentarily carry both: the stored format plus an incoming one (e.g. editing + // a lexical post by sending mobiledoc). Resolve to a single format here; any remaining + // mobiledoc is converted to lexical further down. if (this.previous('mobiledoc') && this.get('lexical')) { + // post is stored as mobiledoc - keep it (explicit conversion happens via convert_to_lexical) this.set('lexical', null); } else if (this.get('mobiledoc') && this.get('lexical')) { - this.set('mobiledoc', null); + // both formats are set on this save - prefer lexical unless mobiledoc is the only + // format that was supplied (e.g. editing a lexical post by sending mobiledoc), in + // which case the incoming mobiledoc wins and is converted to lexical below + if (this.hasChanged('mobiledoc') && !this.hasChanged('lexical')) { + this.set('lexical', null); + } else { + this.set('mobiledoc', null); + } } // CASE: disallow published -> scheduled @@ -688,17 +695,14 @@ Post = ghostBookshelf.Model.extend({ this.set('lexical', JSON.stringify(lexicalLib.blankDocument)); } - // If we're force re-rendering we want to make sure that all image cards - // have original dimensions stored in the payload for use by card renderers - if (options.force_rerender && this.get('mobiledoc')) { - this.set('mobiledoc', await mobiledocLib.populateImageSizes(this.get('mobiledoc'))); - } - - // CASE: mobiledoc has changed, generate html + // CASE: content is still stored as mobiledoc, convert it to lexical so it can be + // rendered. We no longer render mobiledoc directly, so this permanently migrates + // the post to lexical and the lexical block below generates the html. + // CASE: mobiledoc has changed // CASE: ?force_rerender=true passed via Admin API // CASE: html is null, but mobiledoc exists (only important for migrations & importing) if ( - !this.get('lexical') && + this.get('mobiledoc') && ( this.hasChanged('mobiledoc') || options.force_rerender @@ -706,11 +710,12 @@ Post = ghostBookshelf.Model.extend({ ) ) { try { - this.set('html', mobiledocLib.render(JSON.parse(this.get('mobiledoc')))); + this.set('lexical', mobiledocToLexical(this.get('mobiledoc'))); + this.set('mobiledoc', null); } catch (err) { throw new errors.ValidationError({ message: tpl(messages.invalidMobiledocStructure), - help: 'https://ghost.org/docs/publishing/' + help: messages.invalidMobiledocStructureHelp }); } } @@ -927,7 +932,7 @@ Post = ghostBookshelf.Model.extend({ let authorId = await this.contextUser(options); const authorExists = await ghostBookshelf.model('User').findOne({id: authorId}, {transacting: options.transacting}); if (!authorExists) { - authorId = (await ghostBookshelf.model('User').getOwnerUser()).get('id'); + authorId = (await ghostBookshelf.model('User').getOwnerUser({transacting: options.transacting})).get('id'); } ops.push(async function updateRevisions() { const revisionModels = await ghostBookshelf.model('PostRevision') diff --git a/ghost/core/core/server/services/email-service/email-renderer.js b/ghost/core/core/server/services/email-service/email-renderer.js index fb508da5fbd..873c8af0c8d 100644 --- a/ghost/core/core/server/services/email-service/email-renderer.js +++ b/ghost/core/core/server/services/email-service/email-renderer.js @@ -18,6 +18,7 @@ const {getEmailDesign} = require('../email-rendering/email-design'); const {registerHelpers} = require('./helpers/register-helpers'); const crypto = require('crypto'); const {getPostAccessFilter} = require('../members/content-gating'); +const {mobiledocToLexical} = require('@tryghost/kg-converters'); /** @import {TemplateDelegate} from 'handlebars' */ const DEFAULT_LOCALE = 'en-gb'; @@ -463,22 +464,18 @@ class EmailRenderer { async renderPostBaseHtml(post, newsletter) { const postUrl = this.#getPostUrl(post); - let html; - if (post.get('lexical')) { - // only lexical's renderer is async - html = await this.#renderers.lexical.render( - post.get('lexical'), - { - target: 'email', - postUrl, - design: this.#getEmailDesign(newsletter) - } - ); - } else { - html = this.#renderers.mobiledoc.render( - JSON.parse(post.get('mobiledoc')), {target: 'email', postUrl} - ); - } + // posts are migrated to lexical on save, but legacy content may still be stored as + // mobiledoc - convert it to lexical so it can be rendered. + const lexical = post.get('lexical') || mobiledocToLexical(post.get('mobiledoc')); + + const html = await this.#renderers.lexical.render( + lexical, + { + target: 'email', + postUrl, + design: this.#getEmailDesign(newsletter) + } + ); return html; } diff --git a/ghost/core/core/server/services/email-service/email-service-wrapper.js b/ghost/core/core/server/services/email-service/email-service-wrapper.js index 992be956d46..194418876a3 100644 --- a/ghost/core/core/server/services/email-service/email-service-wrapper.js +++ b/ghost/core/core/server/services/email-service/email-service-wrapper.js @@ -40,7 +40,6 @@ class EmailServiceWrapper { const labs = require('../../../shared/labs'); const emailAddressService = require('../email-address'); const i18nLib = require('@tryghost/i18n'); - const mobiledocLib = require('../../lib/mobiledoc'); const lexicalLib = require('../../lib/lexical'); const urlUtils = require('../../../shared/url-utils'); const memberAttribution = require('../member-attribution'); @@ -78,7 +77,6 @@ class EmailServiceWrapper { settingsCache, settingsHelpers, renderers: { - mobiledoc: mobiledocLib, lexical: lexicalLib }, imageSize: cachedImageSizeFromUrl, diff --git a/ghost/core/package.json b/ghost/core/package.json index 4bd0b2204cf..855a59fdf7c 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -113,13 +113,11 @@ "@tryghost/kg-card-factory": "5.2.2", "@tryghost/kg-clean-basic-html": "catalog:", "@tryghost/kg-converters": "catalog:", - "@tryghost/kg-default-atoms": "5.2.2", "@tryghost/kg-default-cards": "10.3.2", "@tryghost/kg-default-nodes": "2.1.2", "@tryghost/kg-html-to-lexical": "1.3.2", "@tryghost/kg-lexical-html-renderer": "1.4.2", "@tryghost/kg-markdown-html-renderer": "7.2.2", - "@tryghost/kg-mobiledoc-html-renderer": "7.2.2", "@tryghost/limit-service": "catalog:", "@tryghost/logging": "catalog:", "@tryghost/members-csv": "2.0.7", @@ -250,7 +248,6 @@ "zod": "catalog:" }, "optionalDependencies": { - "@tryghost/html-to-mobiledoc": "3.3.2", "sqlite3": "5.1.7" }, "devDependencies": { diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap index 49c8523d1f0..8f747c2c68a 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap @@ -24415,7 +24415,7 @@ exports[`Activity Feed API Returns signup events in activity feed 2: [headers] 1 Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "22071", + "content-length": "21607", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap index 6b75f260ba8..d6f669de2c2 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/email-previews.test.js.snap @@ -1698,7 +1698,7 @@ table.body h2 span { -

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
+

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
@@ -1976,7 +1976,7 @@ exports[`Email Preview API Read can read post email preview with fields 4: [head Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "40965", + "content-length": "40907", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap index 567683e008f..c665564c52e 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -431,7 +431,7 @@ exports[`Members API - member attribution Returns sign up attributions of all ty Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "8869", + "content-length": "8753", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap index 14d831ca993..1755f06f7c4 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap @@ -38,10 +38,10 @@ Object { "featured": false, "frontmatter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "lexical": null, + "lexical": "{\\"root\\":{\\"children\\":[{\\"type\\":\\"markdown\\",\\"markdown\\":\\"

Welcome to my invisible post!

\\"}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}", "meta_description": null, "meta_title": null, - "mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[[\\"markdown\\",{\\"markdown\\":\\"

Welcome to my invisible post!

\\"}]],\\"sections\\":[[10,0]]}", + "mobiledoc": null, "newsletter": null, "og_description": null, "og_image": null, @@ -127,10 +127,10 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "featured": false, "frontmatter": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "lexical": null, + "lexical": "{\\"root\\":{\\"children\\":[{\\"type\\":\\"markdown\\",\\"markdown\\":\\"

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
\\"}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}", "meta_description": "meta description for draft post", "meta_title": null, - "mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[[\\"markdown\\",{\\"markdown\\":\\"

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
\\"}]],\\"sections\\":[[10,0]]}", + "mobiledoc": null, "newsletter": null, "og_description": null, "og_image": null, @@ -197,7 +197,7 @@ exports[`Posts API Can browse 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "11412", + "content-length": "11470", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -584,12 +584,12 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

Welcome to my invisible post!

", + "html": "

Welcome to my invisible post!

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "lexical": null, + "lexical": "{\\"root\\":{\\"children\\":[{\\"type\\":\\"markdown\\",\\"markdown\\":\\"

Welcome to my invisible post!

\\"}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}", "meta_description": null, "meta_title": null, - "mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[[\\"markdown\\",{\\"markdown\\":\\"

Welcome to my invisible post!

\\"}]],\\"sections\\":[[10,0]]}", + "mobiledoc": null, "newsletter": null, "og_description": null, "og_image": null, @@ -675,12 +675,12 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "lexical": null, + "lexical": "{\\"root\\":{\\"children\\":[{\\"type\\":\\"markdown\\",\\"markdown\\":\\"

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
\\"}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}", "meta_description": "meta description for draft post", "meta_title": null, - "mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[[\\"markdown\\",{\\"markdown\\":\\"

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

#header h1 a{display: block;width: 300px;height: 80px;}
\\"}]],\\"sections\\":[[10,0]]}", + "mobiledoc": null, "newsletter": null, "og_description": null, "og_image": null, @@ -766,7 +766,7 @@ exports[`Posts API Can browse with formats 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "14210", + "content-length": "14152", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1346,10 +1346,10 @@ Object { "frontmatter": null, "html": "

Testing post creation with mobiledoc

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "lexical": null, + "lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Testing post creation with mobiledoc\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}", "meta_description": null, "meta_title": null, - "mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"ghostVersion\\":\\"4.0\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[],\\"sections\\":[[1,\\"p\\",[[0,[],0,\\"Testing post creation with mobiledoc\\"]]]]}", + "mobiledoc": null, "newsletter": null, "og_description": null, "og_image": null, @@ -1416,7 +1416,7 @@ exports[`Posts API Create Can create a post with mobiledoc 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "4290", + "content-length": "4484", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2353,10 +2353,10 @@ Object { "frontmatter": null, "html": "

Original text

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "lexical": null, + "lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Original text\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}", "meta_description": null, "meta_title": null, - "mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"ghostVersion\\":\\"4.0\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[],\\"sections\\":[[1,\\"p\\",[[0,[],0,\\"Original text\\"]]]]}", + "mobiledoc": null, "newsletter": null, "og_description": null, "og_image": null, @@ -2423,7 +2423,7 @@ exports[`Posts API Update Can update a post with mobiledoc 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "4235", + "content-length": "4429", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2462,10 +2462,10 @@ Object { "frontmatter": null, "html": "

Updated text

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "lexical": null, + "lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[{\\"detail\\":0,\\"format\\":0,\\"mode\\":\\"normal\\",\\"style\\":\\"\\",\\"text\\":\\"Updated text\\",\\"type\\":\\"text\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":\\"ltr\\",\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}", "meta_description": null, "meta_title": null, - "mobiledoc": "{\\"version\\":\\"0.3.1\\",\\"ghostVersion\\":\\"4.0\\",\\"markups\\":[],\\"atoms\\":[],\\"cards\\":[],\\"sections\\":[[1,\\"p\\",[[0,[],0,\\"Updated text\\"]]]]}", + "mobiledoc": null, "newsletter": null, "og_description": null, "og_image": null, @@ -2532,7 +2532,7 @@ exports[`Posts API Update Can update a post with mobiledoc 4: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "4232", + "content-length": "4426", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/admin/pages-legacy.test.js b/ghost/core/test/e2e-api/admin/pages-legacy.test.js index dc53f299b6b..fad1e1369ff 100644 --- a/ghost/core/test/e2e-api/admin/pages-legacy.test.js +++ b/ghost/core/test/e2e-api/admin/pages-legacy.test.js @@ -129,8 +129,9 @@ describe('Pages API', function () { const additionalProperties = ['reading_time']; localUtils.API.checkResponse(returnedPage, 'page', additionalProperties); - assert.equal(returnedPage.mobiledoc, page.mobiledoc); - assert.equal(returnedPage.lexical, null); + // mobiledoc input is converted to lexical on save + assert.equal(returnedPage.mobiledoc, null); + assert.ok(returnedPage.lexical.includes('Testing post creation with mobiledoc')); }); it('Can add a page with lexical', async function () { diff --git a/ghost/core/test/e2e-api/admin/posts-legacy.test.js b/ghost/core/test/e2e-api/admin/posts-legacy.test.js index 99363b0f794..d565f8e2dd1 100644 --- a/ghost/core/test/e2e-api/admin/posts-legacy.test.js +++ b/ghost/core/test/e2e-api/admin/posts-legacy.test.js @@ -1,7 +1,5 @@ const assert = require('node:assert/strict'); const {assertExists} = require('../../utils/assertions'); -const nock = require('nock'); -const path = require('path'); const supertest = require('supertest'); const _ = require('lodash'); const moment = require('moment-timezone'); @@ -414,15 +412,8 @@ describe('Posts API', function () { }); it('Can update and force re-render', async function () { - const unsplashMock = nock('https://images.unsplash.com/') - .get('/favicon_too_large') - .query(true) - .replyWithFile(200, path.join(__dirname, '../../utils/fixtures/images/ghost-logo.png'), { - 'Content-Type': 'image/png' - }); - const mobiledoc = JSON.parse(testUtils.DataGenerator.Content.posts[3].mobiledoc); - mobiledoc.cards.push(['image', {src: 'https://images.unsplash.com/favicon_too_large?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ'}]); + mobiledoc.cards.push(['image', {src: 'https://example.com/image.jpg'}]); mobiledoc.sections.push([10, mobiledoc.cards.length - 1]); const post = { @@ -437,7 +428,7 @@ describe('Posts API', function () { post.updated_at = res.body.posts[0].updated_at; const res2 = await request - .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[3].id + '/?force_rerender=true&formats=mobiledoc,html')) + .put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[3].id + '/?force_rerender=true&formats=mobiledoc,lexical,html')) .set('Origin', config.get('url')) .send({posts: [post]}) .expect('Content-Type', /json/) @@ -447,16 +438,10 @@ describe('Posts API', function () { const expectedPattern = `/p/${uuid}/, /p/${uuid}/?member_status=anonymous, /p/${uuid}/?member_status=free, /p/${uuid}/?member_status=paid`; assert.equal(res2.headers['x-cache-invalidate'], expectedPattern); - assert.equal(unsplashMock.isDone(), true); - - // mobiledoc is updated with image sizes - const resMobiledoc = JSON.parse(res2.body.posts[0].mobiledoc); - const cardPayload = resMobiledoc.cards[mobiledoc.cards.length - 1][1]; - assert.equal(cardPayload.width, 800); - assert.equal(cardPayload.height, 257); - - // html is re-rendered to include srcset - assert.match(res2.body.posts[0].html, /srcset="https:\/\/images\.unsplash\.com\/favicon_too_large\?ixlib=rb-1\.2\.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=600&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ 600w, https:\/\/images\.unsplash\.com\/favicon_too_large\?ixlib=rb-1\.2\.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=800&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ 800w"/); + // mobiledoc input is converted to lexical and the html is re-rendered + assert.equal(res2.body.posts[0].mobiledoc, null); + assert.match(res2.body.posts[0].html, /kg-image-card/); + assert.ok(res2.body.posts[0].html.includes('https://example.com/image.jpg')); }); it('Can unpublish a post', async function () { diff --git a/ghost/core/test/e2e-api/admin/posts.test.js b/ghost/core/test/e2e-api/admin/posts.test.js index 6ec53338da4..e9fd32a0bd1 100644 --- a/ghost/core/test/e2e-api/admin/posts.test.js +++ b/ghost/core/test/e2e-api/admin/posts.test.js @@ -412,7 +412,7 @@ describe('Posts API', function () { await agent .put(`/posts/${postResponse.id}/?formats=mobiledoc,lexical,html`) - .body({posts: [Object.assign({}, postResponse, {mobiledoc: updatedMobiledoc})]}) + .body({posts: [Object.assign({}, postResponse, {mobiledoc: updatedMobiledoc, lexical: null})]}) .expectStatus(200) .matchBodySnapshot({ posts: [Object.assign({}, matchPostShallowIncludes, {published_at: null})] @@ -423,23 +423,22 @@ describe('Posts API', function () { 'x-cache-invalidate': stringMatching(/^\/p\/[a-z0-9-]+\/, \/p\/[a-z0-9-]+\/\?member_status=anonymous, \/p\/[a-z0-9-]+\/\?member_status=free, \/p\/[a-z0-9-]+\/\?member_status=paid$/) }); - // mobiledoc revisions are created + // mobiledoc input is converted to lexical on save, so no mobiledoc revisions are created const mobiledocRevisions = await models.MobiledocRevision .where('post_id', postResponse.id) .orderBy('created_at_ts', 'desc') .fetchAll(); - assert.equal(mobiledocRevisions.length, 2); - assert.equal(mobiledocRevisions.at(0).get('mobiledoc'), updatedMobiledoc); - assert.equal(mobiledocRevisions.at(1).get('mobiledoc'), originalMobiledoc); + assert.equal(mobiledocRevisions.length, 0); - // post revisions are not created + // content is converted to lexical, so the initial lexical post revision is + // created instead of a mobiledoc revision (the update omits save_revision) const postRevisions = await models.PostRevision .where('post_id', postResponse.id) .orderBy('created_at_ts', 'desc') .fetchAll(); - assert.equal(postRevisions.length, 0); + assert.equal(postRevisions.length, 1); }); it('Can update a post with lexical', async function () { diff --git a/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap b/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap index bf20541369e..75f4ef5cf5c 100644 --- a/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap +++ b/ghost/core/test/e2e-api/content/__snapshots__/pages.test.js.snap @@ -21,7 +21,7 @@ Hopefully you don't find it a bore.", "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

Static page test is what this is for.

Hopefully you don't find it a bore.

", + "html": "

Static page test is what this is for.

Hopefully you don't find it a bore.

", "id": "618ba1ffbe2896088840a6e9", "meta_description": null, "meta_title": null, @@ -49,7 +49,7 @@ exports[`Pages Content API Can request page 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "1118", + "content-length": "1060", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -136,7 +136,7 @@ If you prefer to use a contact form, almost all of the great embedded form servi "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

If you want to set up a contact page for people to be able to reach out to you, the simplest way is to set up a simple page like this and list the different ways people can reach out to you.

For example, here's how to reach us!

If you prefer to use a contact form, almost all of the great embedded form services work great with Ghost and are easy to set up:

", + "html": "

If you want to set up a contact page for people to be able to reach out to you, the simplest way is to set up a simple page like this and list the different ways people can reach out to you.

For example, here's how to reach us!

If you prefer to use a contact form, almost all of the great embedded form services work great with Ghost and are easy to set up:

\\"\\"
", "id": "6194d3ce51e2700162531a79", "meta_description": null, "meta_title": null, @@ -253,7 +253,7 @@ Hopefully you don't find it a bore.", "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

Static page test is what this is for.

Hopefully you don't find it a bore.

", + "html": "

Static page test is what this is for.

Hopefully you don't find it a bore.

", "id": "618ba1ffbe2896088840a6e9", "meta_description": null, "meta_title": null, @@ -281,7 +281,7 @@ exports[`Pages Content API Can request pages 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "9408", + "content-length": "9355", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap index 0611b07fd0d..78463810915 100644 --- a/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-api/content/__snapshots__/posts.test.js.snap @@ -659,7 +659,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
+      "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

\\"\\"
Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

\\"\\"
Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
 <article class=\\"article {{post_class}}\\">
 
     <h1>{{title}}</h1>
@@ -698,7 +698,7 @@ exports[`Posts Content API Can filter by published date 4: [headers] 1`] = `
 Object {
   "access-control-allow-origin": "*",
   "cache-control": "public, max-age=0",
-  "content-length": "5908",
+  "content-length": "5918",
   "content-type": "application/json; charset=utf-8",
   "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
   "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
@@ -909,7 +909,7 @@ Object {
       "feature_image_caption": null,
       "featured": false,
       "frontmatter": null,
-      "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
+      "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

\\"\\"
Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

\\"\\"
Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
 <article class=\\"article {{post_class}}\\">
 
     <h1>{{title}}</h1>
@@ -1003,7 +1003,7 @@ Object {
       "feature_image_caption": null,
       "featured": false,
       "frontmatter": null,
-      "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you  start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", + "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
\\"\\"
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.
\\"\\"

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

\\"\\"
Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

\\"\\"

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

\\"\\"

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", "id": "6194d3ce51e2700162531a75", "meta_description": null, "meta_title": null, @@ -1085,7 +1085,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", + "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

\\"\\"

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", "id": "6194d3ce51e2700162531a74", "meta_description": null, "meta_title": null, @@ -1173,7 +1173,7 @@ Using subscrip", "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", + "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

\\"\\"
The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", "id": "6194d3ce51e2700162531a73", "meta_description": null, "meta_title": null, @@ -1266,7 +1266,7 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", + "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", "id": "6194d3ce51e2700162531a72", "meta_description": null, "meta_title": null, @@ -1348,8 +1348,8 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

-

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", + "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

\\"\\"

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

+

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

\\"\\"

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", "id": "6194d3ce51e2700162531a71", "meta_description": null, "meta_title": null, @@ -1669,83 +1669,159 @@ Gallery card: File card: +Test DocumentA test PDF documentdocument.pdf0 Bytedownload-circle +Video card: -Test Document -A test PDF document -document.pdf -0 Byte -download-circle -Video card: -0:00/1× -Audio card: -Test Audio0:00/3:001× -Inserted snippet content below: -This snippet contains all media types for testing URL transformations in reusable content. -Image card: -File card: -Snippet Doc", +0:00 + +/2:00 + + +1× + + + + + + + + + + + + + + + + + +Audio card: + +Test Audio0:00/1801× + +Inserted snippet content below: + +This snippet contains all media types for testing URL transformations in reusable content. + +Image car", "feature_image": "http://127.0.0.1:2369/content/images/feature.jpg", "feature_image_alt": null, "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

-
- -
-
Test Document
-
A test PDF document
-
-
document.pdf
-
0 Byte
+ "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

Video card:

+
+ +
+ +
+
+
+ + + 0:00 +
+ /2:00 +
+ + + + +
-
- download-circle +
+
Test video file
+

Audio card:

\\"audio-thumbnail\\"
Test Audio
0:00
/180

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

Video card:

+
+ +
+
- -
-

Video card:

0:00
/
Test video file

Audio card:

\\"audio-thumbnail\\"
Test Audio
0:00
/3:00

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

-
- -
-
Snippet Document
-
A test document file
-
-

Video card:

0:00
/
Snippet video file

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/2:00

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", +
+
Snippet video file
+

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/120

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", "id": "618ba1ffbe2896088840a700", "meta_description": null, "meta_title": null, @@ -1860,7 +1936,7 @@ Definition listConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", + "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", "id": "618ba1ffbe2896088840a6e7", "meta_description": null, "meta_title": null, @@ -1951,14 +2027,14 @@ mctesters "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

testing

+ "html": "

testing

mctesters

  • test
  • line
  • items
-", +", "id": "618ba1ffbe2896088840a6e3", "meta_description": "meta description for short and sweet", "meta_title": null, @@ -2042,7 +2118,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6e1", "meta_description": null, "meta_title": null, @@ -2124,7 +2200,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6df", "meta_description": null, "meta_title": null, @@ -2173,7 +2249,7 @@ exports[`Posts Content API Can filter posts by authors 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "100264", + "content-length": "102399", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2223,7 +2299,7 @@ Definition listConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", + "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", "id": "618ba1ffbe2896088840a6e7", "meta_description": null, "meta_title": null, @@ -2270,14 +2346,14 @@ mctesters "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

testing

+ "html": "

testing

mctesters

  • test
  • line
  • items
-", +", "id": "618ba1ffbe2896088840a6e3", "meta_description": "meta description for short and sweet", "meta_title": null, @@ -2359,7 +2435,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6e1", "meta_description": null, "meta_title": null, @@ -2460,7 +2536,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6df", "meta_description": null, "meta_title": null, @@ -2552,7 +2628,7 @@ exports[`Posts Content API Can filter posts by tag 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "13979", + "content-length": "13754", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -2673,7 +2749,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
+      "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

\\"\\"
Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

\\"\\"
Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
 <article class=\\"article {{post_class}}\\">
 
     <h1>{{title}}</h1>
@@ -2766,7 +2842,7 @@ Object {
       "feature_image_caption": null,
       "featured": false,
       "frontmatter": null,
-      "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you  start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", + "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
\\"\\"
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.
\\"\\"

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

\\"\\"
Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

\\"\\"

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

\\"\\"

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", "id": "6194d3ce51e2700162531a75", "meta_description": null, "meta_title": null, @@ -2847,7 +2923,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", + "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

\\"\\"

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", "id": "6194d3ce51e2700162531a74", "meta_description": null, "meta_title": null, @@ -2934,7 +3010,7 @@ Using subscrip", "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", + "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

\\"\\"
The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", "id": "6194d3ce51e2700162531a73", "meta_description": null, "meta_title": null, @@ -3026,7 +3102,7 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", + "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", "id": "6194d3ce51e2700162531a72", "meta_description": null, "meta_title": null, @@ -3107,8 +3183,8 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

-

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", + "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

\\"\\"

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

+

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

\\"\\"

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", "id": "6194d3ce51e2700162531a71", "meta_description": null, "meta_title": null, @@ -3426,103 +3502,179 @@ Gallery card: File card: +Test DocumentA test PDF documentdocument.pdf0 Bytedownload-circle +Video card: -Test Document -A test PDF document -document.pdf -0 Byte -download-circle -Video card: -0:00/1× -Audio card: -Test Audio0:00/3:001× -Inserted snippet content below: -This snippet contains all media types for testing URL transformations in reusable content. -Image card: -File card: -Snippet Doc", +0:00 + +/2:00 + + +1× + + + + + + + + + + + + + + + + + +Audio card: + +Test Audio0:00/1801× + +Inserted snippet content below: + +This snippet contains all media types for testing URL transformations in reusable content. + +Image car", "feature_image": "http://127.0.0.1:2369/content/images/feature.jpg", "feature_image_alt": null, "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

-
- -
-
Test Document
-
A test PDF document
-
-
document.pdf
-
0 Byte
-
-
-
- download-circle + "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

Video card:

+
+ +
+
- -
-

Video card:

0:00
/
Test video file

Audio card:

\\"audio-thumbnail\\"
Test Audio
0:00
/3:00

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

-

Audio card:

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

Video card:

+
+ +
+
- -
-

Video card:

0:00
/
Snippet video file

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/2:00

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", - "id": "618ba1ffbe2896088840a700", - "meta_description": null, - "meta_title": null, - "og_description": null, - "og_image": "http://127.0.0.1:2369/content/images/og.jpg", - "og_title": null, - "primary_author": Object { - "bio": "bio", - "bluesky": null, - "cover_image": null, - "facebook": null, - "id": "5951f5fc0000000000000000", - "instagram": null, - "linkedin": null, - "location": "location", - "mastodon": null, - "meta_description": null, - "meta_title": null, - "name": "Joe Bloggs", - "profile_image": "https://example.com/super_photo.jpg", +
+
+ + + 0:00 +
+ /1:00 +
+ + + + + +
+
+
+
Snippet video file
+

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/120

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", + "id": "618ba1ffbe2896088840a700", + "meta_description": null, + "meta_title": null, + "og_description": null, + "og_image": "http://127.0.0.1:2369/content/images/og.jpg", + "og_title": null, + "primary_author": Object { + "bio": "bio", + "bluesky": null, + "cover_image": null, + "facebook": null, + "id": "5951f5fc0000000000000000", + "instagram": null, + "linkedin": null, + "location": "location", + "mastodon": null, + "meta_description": null, + "meta_title": null, + "name": "Joe Bloggs", + "profile_image": "https://example.com/super_photo.jpg", "slug": "joe-bloggs", "threads": null, "tiktok": null, @@ -3594,7 +3746,7 @@ Definition listConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", + "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", "id": "618ba1ffbe2896088840a6e7", "meta_description": null, "meta_title": null, @@ -3664,14 +3816,14 @@ mctesters "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

testing

+ "html": "

testing

mctesters

  • test
  • line
  • items
-", +", "id": "618ba1ffbe2896088840a6e3", "meta_description": "meta description for short and sweet", "meta_title": null, @@ -3754,7 +3906,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6e1", "meta_description": null, "meta_title": null, @@ -3835,7 +3987,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6df", "meta_description": null, "meta_title": null, @@ -3906,7 +4058,7 @@ exports[`Posts Content API Can include relations 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "113199", + "content-length": "115334", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -3935,7 +4087,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6df", "meta_description": null, "meta_title": null, @@ -3962,7 +4114,7 @@ exports[`Posts Content API Can request a single post 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "2357", + "content-length": "2299", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -4106,7 +4258,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
+      "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

\\"\\"
Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

\\"\\"
Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
 <article class=\\"article {{post_class}}\\">
 
     <h1>{{title}}</h1>
@@ -4154,7 +4306,7 @@ Object {
       "feature_image_caption": null,
       "featured": false,
       "frontmatter": null,
-      "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you  start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", + "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
\\"\\"
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.
\\"\\"

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

\\"\\"
Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

\\"\\"

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

\\"\\"

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", "id": "6194d3ce51e2700162531a75", "meta_description": null, "meta_title": null, @@ -4190,7 +4342,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", + "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

\\"\\"

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", "id": "6194d3ce51e2700162531a74", "meta_description": null, "meta_title": null, @@ -4232,7 +4384,7 @@ Using subscrip", "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", + "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

\\"\\"
The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", "id": "6194d3ce51e2700162531a73", "meta_description": null, "meta_title": null, @@ -4279,7 +4431,7 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", + "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", "id": "6194d3ce51e2700162531a72", "meta_description": null, "meta_title": null, @@ -4315,8 +4467,8 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

-

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", + "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

\\"\\"

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

+

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

\\"\\"

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", "id": "6194d3ce51e2700162531a71", "meta_description": null, "meta_title": null, @@ -4544,83 +4696,159 @@ Gallery card: File card: +Test DocumentA test PDF documentdocument.pdf0 Bytedownload-circle +Video card: -Test Document -A test PDF document -document.pdf -0 Byte -download-circle -Video card: -0:00/1× -Audio card: -Test Audio0:00/3:001× -Inserted snippet content below: -This snippet contains all media types for testing URL transformations in reusable content. -Image card: -File card: -Snippet Doc", +0:00 + +/2:00 + + +1× + + + + + + + + + + + + + + + + + +Audio card: + +Test Audio0:00/1801× + +Inserted snippet content below: + +This snippet contains all media types for testing URL transformations in reusable content. + +Image car", "feature_image": "http://127.0.0.1:2369/content/images/feature.jpg", "feature_image_alt": null, "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

-
- -
-
Test Document
-
A test PDF document
-
-
document.pdf
-
0 Byte
+ "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

Video card:

+
+ +
+ +
+
+
+ + + 0:00 +
+ /2:00 +
+ + + + +
-
- download-circle +
+
Test video file
+

Audio card:

\\"audio-thumbnail\\"
Test Audio
0:00
/180

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

Video card:

+
+ +
+
- -
-

Video card:

0:00
/
Test video file

Audio card:

\\"audio-thumbnail\\"
Test Audio
0:00
/3:00

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

-
- -
-
Snippet Document
-
A test document file
-
-

Video card:

0:00
/
Snippet video file

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/2:00

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", +
+
Snippet video file
+

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/120

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", "id": "618ba1ffbe2896088840a700", "meta_description": null, "meta_title": null, @@ -4667,7 +4895,7 @@ Definition listConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", + "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", "id": "618ba1ffbe2896088840a6e7", "meta_description": null, "meta_title": null, @@ -4712,14 +4940,14 @@ mctesters "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

testing

+ "html": "

testing

mctesters

  • test
  • line
  • items
-", +", "id": "618ba1ffbe2896088840a6e3", "meta_description": "meta description for short and sweet", "meta_title": null, @@ -4757,7 +4985,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6e1", "meta_description": null, "meta_title": null, @@ -4793,7 +5021,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6df", "meta_description": null, "meta_title": null, @@ -4820,7 +5048,7 @@ exports[`Posts Content API Can request posts 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "87282", + "content-length": "89417", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -4895,7 +5123,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
+      "html": "

As discussed in the introduction post, one of the best things about Ghost is just how much you can customize to turn your site into something unique. Everything about your layout and design can be changed, so you're not stuck with yet another clone of a social network profile.

How far you want to go with customization is completely up to you, there's no right or wrong approach! The majority of people use one of Ghost's built-in themes to get started, and then progress to something more bespoke later on as their site grows.

The best way to get started is with Ghost's branding settings, where you can set up colors, images and logos to fit with your brand.

\\"\\"
Ghost Admin → Settings → Branding

Any Ghost theme that's up to date and compatible with Ghost 4.0 and higher will reflect your branding settings in the preview window, so you can see what your site will look like as you experiment with different options.

When selecting an accent color, try to choose something which will contrast well with white text. Many themes will use your accent color as the background for buttons, headers and navigational elements. Vibrant colors with a darker hue tend to work best, as a general rule.

Installing Ghost themes

By default, new sites are created with Ghost's friendly publication theme, called Casper. Everything in Casper is optimized to work for the most common types of blog, newsletter and publication that people create with Ghost — so it's a perfect place to start.

However, there are hundreds of different themes available to install, so you can pick out a look and feel that suits you best.

\\"\\"
Ghost Admin → Settings → Theme

Inside Ghost's theme settings you'll find 4 more official themes that can be directly installed and activated. Each theme is suited to slightly different use-cases.

  • Casper (default) — Made for all sorts of blogs and newsletters
  • Edition — A beautiful minimal template for newsletter authors
  • Alto — A slick news/magazine style design for creators
  • London — A light photography theme with a bold grid
  • Ease — A library theme for organizing large content archives

And if none of those feel quite right, head on over to the Ghost Marketplace, where you'll find a huge variety of both free and premium themes.

Building something custom

Finally, if you want something completely bespoke for your site, you can always build a custom theme from scratch and upload it to your site.

Ghost's theming template files are very easy to work with, and can be picked up in the space of a few hours by anyone who has just a little bit of knowledge of HTML and CSS. Templates from other platforms can also be ported to Ghost with relatively little effort.

If you want to take a quick look at the theme syntax to see what it's like, you can browse through the files of the default Casper theme. We've added tons of inline code comments to make it easy to learn, and the structure is very readable.

{{#post}}
 <article class=\\"article {{post_class}}\\">
 
     <h1>{{title}}</h1>
@@ -4943,7 +5171,7 @@ Object {
       "feature_image_caption": null,
       "featured": false,
       "frontmatter": null,
-      "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you  start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", + "html": "

Ghost comes with a best-in-class editor which does its very best to get out of the way, and let you focus on your content. Don't let its minimal looks fool you, though, beneath the surface lies a powerful editing toolset designed to accommodate the extensive needs of modern creators.

For many, the base canvas of the Ghost editor will feel familiar. You can start writing as you would expect, highlight content to access the toolbar you would expect, and generally use all of the keyboard shortcuts you would expect.

Our main focus in building the Ghost editor is to try and make as many things that you hope/expect might work: actually work.

  • You can copy and paste raw content from web pages, and Ghost will do its best to correctly preserve the formatting.
  • Pasting an image from your clipboard will upload inline.
  • Pasting a social media URL will automatically create an embed.
  • Highlight a word in the editor and paste a URL from your clipboard on top: Ghost will turn it into a link.
  • You can also paste (or write!) Markdown and Ghost will usually be able to auto-convert it into fully editable, formatted content.
\\"\\"
The Ghost editor. Also available in dark-mode, for late night writing sessions.

The goal, as much as possible, is for things to work so that you don't have to think so much about the editor. You won't find any disastrous \\"block builders\\" here, where you have to open 6 submenus and choose from 18 different but identical alignment options. That's not what Ghost is about.

What you will find though, is dynamic cards which allow you to embed rich media into your posts and create beautifully laid out stories.

Using cards

You can insert dynamic cards inside post content using the + button, which appears on new lines, or by typing / on a new line to trigger the card menu. Many of the choices are simple and intuitive, like bookmark cards, which allow you to create rich links with embedded structured data:

Open Subscription Platforms
A shared movement for independent subscription data.
\\"\\"Open Subscription Platforms
\\"\\"

or embed cards which make it easy to insert content you want to share with your audience, from external services:

But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries.

Once you start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format.
\\"\\"

As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you.

\\"\\"
Peaceful places

Galleries and image cards can be combined in so many different ways — the only limit is your imagination.

Build workflows with snippets

One of the most powerful features of the Ghost editor is the ability to create and re-use content snippets. If you've ever used an email client with a concept of saved replies then this will be immediately intuitive.

To create a snippet, select a piece of content in the editor that you'd like to re-use in future, then click on the snippet icon in the toolbar. Give your snippet a name, and you're all done. Now your snippet will be available from within the card menu, or you can search for it directly using the / command.

This works really well for saving images you might want to use often, like a company logo or team photo, links to resources you find yourself often linking to, or introductions and passages that you want to remember.

\\"\\"

You can even build entire post templates or outlines to create a quick, re-usable workflow for publishing over time. Or build custom design elements for your post with an HTML card, and use a snippet to insert it.

Once you get a few useful snippets set up, it's difficult to go back to the old way of diving through media libraries and trawling for that one thing you know you used somewhere that one time.


Publishing and newsletters the easy way

When you're ready to publish, Ghost makes it as simple as possible to deliver your new post to all your existing members. Just hit the Preview link and you'll get a chance to see what your content looks like on Web, Mobile, Email and Social.

\\"\\"

You can send yourself a test newsletter to make sure everything looks good in your email client, and then hit the Publish button to decide who to deliver it to.

Ghost comes with a streamlined, optimized email newsletter template that has settings built-in for you to customize the colors and typography. We've spent countless hours refining the template to make sure it works great across all email clients, and performs well for email deliverability.

So, you don't need to fight the awful process of building a custom email template from scratch. It's all done already!


The Ghost editor is powerful enough to do whatever you want it to do. With a little exploration, you'll be up and running in no time.

", "id": "6194d3ce51e2700162531a75", "meta_description": null, "meta_title": null, @@ -4979,7 +5207,7 @@ Object { "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", + "html": "

What sets Ghost apart from other products is that you can publish content and grow your audience using the same platform. Rather than just endlessly posting and hoping someone is listening, you can track real signups against your work and have them subscribe to be notified of future posts. The feature that makes all this possible is called Portal.

Portal is an embedded interface for your audience to sign up to your site. It works on every Ghost site, with every theme, and for any type of publisher.

You can customize the design, content and settings of Portal to suit your site, whether you just want people to sign up to your newsletter — or you're running a full premium publication with user sign-ins and private content.

\\"\\"

Once people sign up to your site, they'll receive an email confirmation with a link to click. The link acts as an automatic sign-in, so subscribers will be automatically signed-in to your site when they click on it. There are a couple of interesting angles to this:

Because subscribers are automatically able to sign in and out of your site as registered members: You can (optionally) restrict access to posts and pages depending on whether people are signed-in or not. So if you want to publish some posts for free, but keep some really great stuff for members-only, this can be a great draw to encourage people to sign up!

Ghost members sign in using email authentication links, so there are no passwords for people to set or forget. You can turn any list of email subscribers into a database of registered members who can sign in to your site. Like magic.

Portal makes all of this possible, and it appears by default as a floating button in the bottom-right corner of your site. When people are logged out, clicking it will open a sign-up/sign-in window. When members are logged in, clicking the Portal button will open the account menu where they can edit their name, email, and subscription settings.

The floating Portal button is completely optional. If you prefer, you can add manual links to your content, navigation, or theme to trigger it instead.

Like this! Sign up here


As you start to grow your registered audience, you'll be able to get a sense of who you're publishing for and where those people are coming from. Best of all: You'll have a straightforward, reliable way to connect with people who enjoy your work.

Social networks go in and out of fashion all the time. Email addresses are timeless.

Growing your audience is valuable no matter what type of site you run, but if your content is your business, then you might also be interested in setting up premium subscriptions.

", "id": "6194d3ce51e2700162531a74", "meta_description": null, "meta_title": null, @@ -5021,7 +5249,7 @@ Using subscrip", "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", + "html": "

For creators and aspiring entrepreneurs looking to generate a sustainable recurring revenue stream from their creative work, Ghost has built-in payments allowing you to create a subscription commerce business.

Connect your Stripe account to Ghost, and you'll be able to quickly and easily create monthly and yearly premium plans for members to subscribe to, as well as complimentary plans for friends and family.

Ghost takes 0% payment fees, so everything you make is yours to keep!

Using subscriptions, you can build an independent media business like Stratechery, The Information, or The Browser.

The creator economy is just getting started, and Ghost allows you to build something based on technology that you own and control.

\\"\\"
The Browser has over 10,000 paying subscribers

Most successful subscription businesses publish a mix of free and paid posts to attract a new audience, and upsell the most loyal members to a premium offering. You can also mix different access levels within the same post, showing a free preview to logged out members and then, right when you're ready for a cliffhanger, that's a good time to...

", "id": "6194d3ce51e2700162531a73", "meta_description": null, "meta_title": null, @@ -5068,7 +5296,7 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", + "html": "

As you grow, you'll probably want to start inviting team members and collaborators to your site. Ghost has a number of different user roles for your team:

Contributors
This is the base user level in Ghost. Contributors can create and edit their own draft posts, but they are unable to edit drafts of others or publish posts. Contributors are untrusted users with the most basic access to your publication.

Authors
Authors are the 2nd user level in Ghost. Authors can write, edit and publish their own posts. Authors are trusted users. If you don't trust users to be allowed to publish their own posts, they should be set as Contributors.

Editors
Editors are the 3rd user level in Ghost. Editors can do everything that an Author can do, but they can also edit and publish the posts of others - as well as their own. Editors can also invite new Contributors & Authors to the site.

Administrators
The top user level in Ghost is Administrator. Again, administrators can do everything that Authors and Editors can do, but they can also edit all site settings and data, not just content. Additionally, administrators have full access to invite, manage or remove any other user of the site.

The Owner
There is only ever one owner of a Ghost site. The owner is a special user which has all the same permissions as an Administrator, but with two exceptions: 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).

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.

If you're looking for insights, tips and reference materials to expand your content business, here's 5 top resources to get you started:

", "id": "6194d3ce51e2700162531a72", "meta_description": null, "meta_title": null, @@ -5104,8 +5332,8 @@ Most successful subscription businesses publish a mix of free and paid posts to "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

-

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", + "html": "

It's possible to extend your Ghost site and connect it with hundreds of the most popular apps and tools using integrations.

Whether you need to automatically publish new posts on social media, connect your favorite analytics tool, sync your community or embed forms into your content — our integrations library has got it all covered with hundreds of integration tutorials.

Many integrations are as simple as inserting an embed by pasting a link, or copying a snippet of code directly from an app and pasting it into Ghost. Our integration tutorials are used by creators of all kinds to get apps and integrations up and running in no time — no technical knowledge required.

\\"\\"

Zapier

Zapier is a no-code tool that allows you to build powerful automations, and our official integration allows you to connect your Ghost site to more than 1,000 external services.

Example: When someone new subscribes to a newsletter on a Ghost site (Trigger) then the contact information is automatically pushed into MailChimp (Action).

Here's a few of the most popular automation templates:

+

Custom integrations

For more advanced automation, it's possible to create custom Ghost integrations with dedicated API keys from the Integrations page within Ghost Admin.

\\"\\"

These custom integrations allow you to use the Ghost API without needing to write code, and create powerful workflows such as sending content from your favorite desktop editor into Ghost as a new draft.

", "id": "6194d3ce51e2700162531a71", "meta_description": null, "meta_title": null, @@ -5333,83 +5561,159 @@ Gallery card: File card: +Test DocumentA test PDF documentdocument.pdf0 Bytedownload-circle +Video card: -Test Document -A test PDF document -document.pdf -0 Byte -download-circle -Video card: -0:00/1× -Audio card: -Test Audio0:00/3:001× -Inserted snippet content below: -This snippet contains all media types for testing URL transformations in reusable content. -Image card: -File card: -Snippet Doc", +0:00 + +/2:00 + + +1× + + + + + + + + + + + + + + + + + +Audio card: + +Test Audio0:00/1801× + +Inserted snippet content below: + +This snippet contains all media types for testing URL transformations in reusable content. + +Image car", "feature_image": "http://127.0.0.1:2369/content/images/feature.jpg", "feature_image_alt": null, "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

-
- -
-
Test Document
-
A test PDF document
-
-
document.pdf
-
0 Byte
+ "html": "

This is a post containing all media types for testing URL transformations. It includes images, galleries, files, videos, audio, and an inserted snippet.

Image card:

\\"Inline
An inline image card

Gallery card:

A gallery with three images

File card:

Video card:

+
+ +
+ +
+
+
+ + + 0:00 +
+ /2:00 +
+ + + + +
-
- download-circle +
+
Test video file
+

Audio card:

\\"audio-thumbnail\\"
Test Audio
0:00
/180

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

Video card:

+
+ +
+
- -
-

Video card:

0:00
/
Test video file

Audio card:

\\"audio-thumbnail\\"
Test Audio
0:00
/3:00

Inserted snippet content below:

This snippet contains all media types for testing URL transformations in reusable content.

Image card:

\\"Snippet
Image in snippet

File card:

-
- -
-
Snippet Document
-
A test document file
-
-

Video card:

0:00
/
Snippet video file

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/2:00

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", +
+
Snippet video file
+

Audio card:

\\"audio-thumbnail\\"
Snippet Audio
0:00
/120

End of inserted snippet content.

This post tests that all URL transformations work correctly across all media types and inserted snippets.

", "id": "618ba1ffbe2896088840a700", "meta_description": null, "meta_title": null, @@ -5456,7 +5760,7 @@ Definition listConsectetur adipisicing elit, sed do eiusmod tempor incididunt ut "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", + "html": "

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.

1234
abcd
efgh
ijkl
Definition list
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Lorem ipsum dolor sit amet
Consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
  • Morbi in sem quis dui placerat ornare. Pellentesque odio nisi, euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras consequat.
  • Praesent dapibus, neque id cursus faucibus, tortor neque egestas augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam dui mi, tincidunt quis, accumsan porttitor, facilisis luctus, metus.
  • Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec consectetuer ligula vulputate sem tristique cursus. Nam nulla quam, gravida non, commodo a, sodales sit amet, nisi.
  • Pellentesque fermentum dolor. Aliquam quam lectus, facilisis auctor, ultrices ut, elementum vulputate, nunc.

", "id": "618ba1ffbe2896088840a6e7", "meta_description": null, "meta_title": null, @@ -5501,14 +5805,14 @@ mctesters "feature_image_caption": null, "featured": true, "frontmatter": null, - "html": "

testing

+ "html": "

testing

mctesters

  • test
  • line
  • items
-", +", "id": "618ba1ffbe2896088840a6e3", "meta_description": "meta description for short and sweet", "meta_title": null, @@ -5546,7 +5850,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6e1", "meta_description": null, "meta_title": null, @@ -5582,7 +5886,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": "618ba1ffbe2896088840a6df", "meta_description": null, "meta_title": null, @@ -5609,7 +5913,7 @@ exports[`Posts Content API Can request posts from different origin 2: [headers] Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "87282", + "content-length": "89417", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -5743,45 +6047,65 @@ Gallery card: File card: +Test DocumentA test PDF documentdocument.pdf0 Bytedownload-circle +Video card: -Test Document -A test PDF document -document.pdf -0 Byte -download-circle -Video card: -0:00/1× -Audio card: -Test Audio0:00/3:001× -Inserted snippet content below: -This snippet contains all media types for testing URL transformations in reusable content. -Image card: -File card: -Snippet Doc", +0:00 + +/2:00 + + +1× + + + + + + + + + + + + + + + + + +Audio card: + +Test Audio0:00/1801× + +Inserted snippet content below: + +This snippet contains all media types for testing URL transformations in reusable content. + +Image car", }, Object { "excerpt": " * Lorem @@ -5960,7 +6284,7 @@ or embed cards which make it easy to insert content you want to share with your But, dig a little deeper, and you'll also find more advanced cards, like one that only shows up in email newsletters (great for personalized introductions) and a comprehensive set of specialized cards for different types of images and galleries. -Once you  start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format. +Once you start mixing text and image cards creatively, the whole narrative of the story changes. Suddenly, you're working in a new format. As it turns out, sometimes pictures and a thousand words go together really well. Telling people a great story often has much more impact if they can feel, even for a moment, as though they were right there with you. @@ -6211,31 +6535,59 @@ Gallery card: File card: +Test DocumentA test PDF documentdocument.pdf0 Bytedownload-circle + +Video card: + + + + + + + + + + + + + + + + + + + + + + + + +0:00 + +/2:00 + + +1× + + -Test Document -A test PDF document -document.pdf -0 Byte -download-circle -Video card: -0:00/1× Audio card: -Test Audio0:00/3:001× +Test Audio0:00/1801× Inserted snippet content below: @@ -6245,31 +6597,59 @@ Image card: File card: +Snippet DocumentA test document filesnippet-doc.pdf0 Bytedownload-circle + +Video card: + + + + + + + + + + + + + + + + + + + + + + + + +0:00 + +/1:00 + + +1× + + -Snippet Document -A test document file -snippet-doc.pdf -0 Byte -download-circle -Video card: -0:00/1× Audio card: -Snippet Audio0:00/2:001× +Snippet Audio0:00/1201× End of inserted snippet content. @@ -6784,45 +7164,65 @@ Gallery card: File card: +Test DocumentA test PDF documentdocument.pdf0 Bytedownload-circle + +Video card: -Test Document -A test PDF document -document.pdf -0 Byte -download-circle -Video card: -0:00/1× -Audio card: -Test Audio0:00/3:001× -Inserted snippet content below: -This snippet contains all media types for testing URL transformations in reusable content. -Image card: -File card: +0:00 + +/2:00 + + +1× + + + + + + + + + + + + + + + + + +Audio card: + +Test Audio0:00/1801× + +Inserted snippet content below: + +This snippet contains all media types for testing URL transformations in reusable content. -Snippet Doc", +Image car", "feature_image": "http://127.0.0.1:2369/content/images/feature.jpg", "feature_image_alt": null, "feature_image_caption": null, diff --git a/ghost/core/test/e2e-api/content/__snapshots__/search-index.test.js.snap b/ghost/core/test/e2e-api/content/__snapshots__/search-index.test.js.snap index e8779d3767b..8535a0ce638 100644 --- a/ghost/core/test/e2e-api/content/__snapshots__/search-index.test.js.snap +++ b/ghost/core/test/e2e-api/content/__snapshots__/search-index.test.js.snap @@ -162,7 +162,7 @@ exports[`Search Index Content API fetchPosts should return a list of posts 2: [h Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "5983", + "content-length": "6003", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/content/posts.test.js b/ghost/core/test/e2e-api/content/posts.test.js index 79652bf1e5e..d61895d4809 100644 --- a/ghost/core/test/e2e-api/content/posts.test.js +++ b/ghost/core/test/e2e-api/content/posts.test.js @@ -64,7 +64,7 @@ describe('Posts Content API', function () { // Assign a newsletter to one of the posts const newsletterId = testUtils.DataGenerator.Content.newsletters[0].id; const postId = testUtils.DataGenerator.Content.posts[0].id; - await models.Post.edit({newsletter_id: newsletterId}, {id: postId}); + await models.Post.edit({newsletter_id: newsletterId}, {id: postId, context: {internal: true}}); }); it('Can request posts', async function () { diff --git a/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap index 028573ac991..373b4f57d33 100644 --- a/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-webhooks/__snapshots__/posts.test.js.snap @@ -591,7 +591,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "meta_description": null, "meta_title": null, @@ -1571,7 +1571,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "meta_description": null, "meta_title": null, @@ -1877,7 +1877,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "meta_description": null, "meta_title": null, @@ -2123,7 +2123,7 @@ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac tu "feature_image_caption": null, "featured": false, "frontmatter": null, - "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", + "html": "

HTML Ipsum Presents

Pellentesque habitant morbi tristique 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. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. Quisque sit amet est et sapien ullamcorper pharetra. Vestibulum erat wisi, condimentum sed, commodo vitae, ornare sit amet, wisi. Aenean fermentum, elit eget tincidunt condimentum, eros ipsum rutrum orci, sagittis tempus lacus enim ac dui. Donec non enim in turpis pulvinar facilisis. Ut felis.

Header Level 2

  1. Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  2. Aliquam tincidunt mauris eu risus.

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.

Header Level 3

  • Lorem ipsum dolor sit amet, consectetuer adipiscing elit.
  • Aliquam tincidunt mauris eu risus.
#header h1 a{display: block;width: 300px;height: 80px;}
", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "meta_description": null, "meta_title": null, diff --git a/ghost/core/test/integration/importer/v1.test.js b/ghost/core/test/integration/importer/v1.test.js index d78eb251303..ebc9eaced72 100644 --- a/ghost/core/test/integration/importer/v1.test.js +++ b/ghost/core/test/integration/importer/v1.test.js @@ -58,7 +58,9 @@ describe('Importer 1.0', function () { }); exportData.data.posts[1].mobiledoc = '{'; - const options = Object.assign({formats: 'mobiledoc,html'}, testUtils.context.internal); + const options = Object.assign({formats: 'mobiledoc,lexical,html'}, testUtils.context.internal); + + const blankLexical = '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}'; return dataImporter.doImport(exportData, importOptions) .then(function () { @@ -68,16 +70,19 @@ describe('Importer 1.0', function () { }).then(function (result) { const posts = result[0].data.map(model => model.toJSON(options)); + // invalid mobiledoc is replaced with a blank document and converted to lexical assert.equal(posts.length, 2); assert.equal(posts[0].html, null); - assert.equal(posts[0].mobiledoc, '{"version":"0.3.1","ghostVersion":"4.0","markups":[],"atoms":[],"cards":[],"sections":[[1,"p",[[0,[],0,""]]]]}'); + assert.equal(posts[0].mobiledoc, null); + assert.equal(posts[0].lexical, blankLexical); assert.equal(posts[1].html, null); - assert.equal(posts[1].mobiledoc, '{"version":"0.3.1","ghostVersion":"4.0","markups":[],"atoms":[],"cards":[],"sections":[[1,"p",[[0,[],0,""]]]]}'); + assert.equal(posts[1].mobiledoc, null); + assert.equal(posts[1].lexical, blankLexical); }); }); - it('mobiledoc is null, html field is set, convert html -> mobiledoc', function () { + it('mobiledoc is null, html field is set, convert html -> lexical', function () { const exportData = exportedBodyV1().db[0]; exportData.data.posts[0] = testUtils.DataGenerator.forKnex.createPost({ @@ -87,7 +92,7 @@ describe('Importer 1.0', function () { exportData.data.posts[0].mobiledoc = null; - const options = Object.assign({formats: 'mobiledoc,html'}, testUtils.context.internal); + const options = Object.assign({formats: 'mobiledoc,lexical,html'}, testUtils.context.internal); return dataImporter.doImport(exportData, importOptions) .then(function () { @@ -99,7 +104,8 @@ describe('Importer 1.0', function () { assert.equal(posts.length, 1); assert.equal(posts[0].html, '

This is my post content.

'); - assert.equal(posts[0].mobiledoc, '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"h1",[[0,[],0,"This is my post content."]]]]}'); + assert.equal(posts[0].mobiledoc, null); + assert.equal(posts[0].lexical, '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"This is my post content.","type":"extended-text","version":1}],"direction":null,"format":"","indent":0,"type":"extended-heading","version":1,"tag":"h1"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); }); }); @@ -140,7 +146,7 @@ describe('Importer 1.0', function () { exportData.data.posts[0].html = null; - const options = Object.assign({formats: 'mobiledoc,html'}, testUtils.context.internal); + const options = Object.assign({formats: 'mobiledoc,lexical,html'}, testUtils.context.internal); return dataImporter.doImport(exportData, importOptions) .then(function () { @@ -150,9 +156,11 @@ describe('Importer 1.0', function () { }).then(function (result) { const posts = result[0].data.map(model => model.toJSON(options)); + // mobiledoc is converted to lexical, the markdown card becomes a markdown node assert.equal(posts.length, 1); - assert.equal(posts[0].html, '

markdown

\n'); - assert.equal(posts[0].mobiledoc, '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["markdown",{"markdown":"## markdown"}]],"sections":[[10,0]],"ghostVersion":"3.0"}'); + assert.equal(posts[0].html, '

markdown

\n'); + assert.equal(posts[0].mobiledoc, null); + assert.equal(posts[0].lexical, '{"root":{"children":[{"type":"markdown","markdown":"## markdown"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); }); }); @@ -165,7 +173,7 @@ describe('Importer 1.0', function () { mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('# This is my post content') }); - const options = Object.assign({formats: 'mobiledoc,html'}, testUtils.context.internal); + const options = Object.assign({formats: 'mobiledoc,lexical,html'}, testUtils.context.internal); return dataImporter.doImport(exportData, importOptions) .then(function () { @@ -175,11 +183,11 @@ describe('Importer 1.0', function () { }).then(function (result) { const posts = result[0].data.map(model => model.toJSON(options)); + // mobiledoc takes precedence over html and is converted to lexical assert.equal(posts.length, 1); - assert.equal(posts[0].html, '

This is my post content

\n'); - const expectedMobiledoc = JSON.parse(exportData.data.posts[0].mobiledoc); - expectedMobiledoc.ghostVersion = '3.0'; - assert.equal(posts[0].mobiledoc, JSON.stringify(expectedMobiledoc)); + assert.equal(posts[0].html, '

This is my post content

\n'); + assert.equal(posts[0].mobiledoc, null); + assert.equal(posts[0].lexical, '{"root":{"children":[{"type":"markdown","markdown":"# This is my post content"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); }); }); @@ -227,7 +235,7 @@ describe('Importer 1.0', function () { html: '

Post Content

\n' }); - const options = Object.assign({formats: 'mobiledoc,html'}, testUtils.context.internal); + const options = Object.assign({formats: 'mobiledoc,lexical,html'}, testUtils.context.internal); return dataImporter.doImport(exportData, importOptions) .then(function () { @@ -237,13 +245,17 @@ describe('Importer 1.0', function () { }).then(function (result) { const posts = result[0].data.map(model => model.toJSON(options)); + // mobiledoc is converted to lexical, the legacy imageStyle is normalised to + // cardWidth before conversion assert.equal(posts.length, 2); - assert.equal(posts[0].mobiledoc, '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["markdown",{"markdown":"## Post Content"}],["image",{"src":"source2","cardWidth":"not-wide"}]],"sections":[[10,0],[10,1]],"ghostVersion":"3.0"}'); - assert.equal(posts[0].html, '

Post Content

\n
'); + assert.equal(posts[0].mobiledoc, null); + assert.equal(posts[0].lexical, '{"root":{"children":[{"type":"markdown","markdown":"## Post Content"},{"type":"image","src":"source2","cardWidth":"not-wide"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); + assert.equal(posts[0].html, '

Post Content

\n
'); - assert.equal(posts[1].mobiledoc, '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["image",{"src":"source","cardWidth":"wide"}],["markdown",{"markdown":"# Post Content"}]],"sections":[[10,0],[10,1]],"ghostVersion":"3.0"}'); - assert.equal(posts[1].html, '

Post Content

\n'); + assert.equal(posts[1].mobiledoc, null); + assert.equal(posts[1].lexical, '{"root":{"children":[{"type":"image","src":"source","cardWidth":"wide"},{"type":"markdown","markdown":"# Post Content"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); + assert.equal(posts[1].html, '

Post Content

\n'); }); }); }); diff --git a/ghost/core/test/integration/importer/v2.test.js b/ghost/core/test/integration/importer/v2.test.js index 2492dea302e..03c65e2cd0a 100644 --- a/ghost/core/test/integration/importer/v2.test.js +++ b/ghost/core/test/integration/importer/v2.test.js @@ -1084,7 +1084,7 @@ describe('Importer', function () { delete exportData.data.posts[0].html; - const options = Object.assign({formats: 'mobiledoc,html'}, testUtils.context.internal); + const options = Object.assign({formats: 'mobiledoc,lexical,html'}, testUtils.context.internal); return dataImporter.doImport(exportData, importOptions) .then(function () { @@ -1096,8 +1096,11 @@ describe('Importer', function () { assert.equal(posts.length, 1); - assert.equal(posts[0].mobiledoc, '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["image",{"src":"source","cardWidth":"wide"}],["markdown",{"markdown":"# Post Content"}]],"sections":[[10,0],[10,1]],"ghostVersion":"3.0"}'); - assert.equal(posts[0].html, '

Post Content

\n'); + // mobiledoc is converted to lexical (the legacy card-markdown card is + // normalised to a markdown node) + assert.equal(posts[0].mobiledoc, null); + assert.equal(posts[0].lexical, '{"root":{"children":[{"type":"image","src":"source","cardWidth":"wide"},{"type":"markdown","markdown":"# Post Content"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); + assert.equal(posts[0].html, '

Post Content

\n'); }); }); @@ -1145,7 +1148,7 @@ describe('Importer', function () { html: '

Post Content

\n' }); - const options = Object.assign({formats: 'mobiledoc,html'}, testUtils.context.internal); + const options = Object.assign({formats: 'mobiledoc,lexical,html'}, testUtils.context.internal); return dataImporter.doImport(exportData, importOptions) .then(function () { @@ -1157,11 +1160,14 @@ describe('Importer', function () { assert.equal(posts.length, 2); - assert.equal(posts[0].mobiledoc, '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["markdown",{"markdown":"## Post Content"}],["image",{"src":"source2","cardWidth":"not-wide"}]],"sections":[[10,0],[10,1]],"ghostVersion":"3.0"}'); - assert.equal(posts[0].html, '

Post Content

\n
'); + // mobiledoc is converted to lexical on import + assert.equal(posts[0].mobiledoc, null); + assert.equal(posts[0].lexical, '{"root":{"children":[{"type":"markdown","markdown":"## Post Content"},{"type":"image","src":"source2","cardWidth":"not-wide"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); + assert.equal(posts[0].html, '

Post Content

\n
'); - assert.equal(posts[1].mobiledoc, '{"version":"0.3.1","markups":[],"atoms":[],"cards":[["image",{"src":"source","cardWidth":"wide"}],["markdown",{"markdown":"# Post Content"}]],"sections":[[10,0],[10,1]],"ghostVersion":"3.0"}'); - assert.equal(posts[1].html, '

Post Content

\n'); + assert.equal(posts[1].mobiledoc, null); + assert.equal(posts[1].lexical, '{"root":{"children":[{"type":"image","src":"source","cardWidth":"wide"},{"type":"markdown","markdown":"# Post Content"}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); + assert.equal(posts[1].html, '

Post Content

\n'); }); }); diff --git a/ghost/core/test/integration/services/email-service/batch-sending.test.js b/ghost/core/test/integration/services/email-service/batch-sending.test.js index 3a24b99f577..7a7eb9f6042 100644 --- a/ghost/core/test/integration/services/email-service/batch-sending.test.js +++ b/ghost/core/test/integration/services/email-service/batch-sending.test.js @@ -47,7 +47,8 @@ function sortBatches(a, b) { async function testEmailBatches(settings, email_recipient_filter, expectedBatches) { const {emailModel} = await sendEmail(agent, settings, email_recipient_filter); - assert.equal(emailModel.get('source_type'), 'mobiledoc'); + // posts created with mobiledoc are converted to lexical on save + assert.equal(emailModel.get('source_type'), 'lexical'); assert(emailModel.get('subject')); assert(emailModel.get('from')); const expectedTotal = expectedBatches.reduce((acc, batch) => acc + batch.recipients, 0); diff --git a/ghost/core/test/legacy/api/admin/pages.test.js b/ghost/core/test/legacy/api/admin/pages.test.js index fc62cea40a5..c04abdfb2fe 100644 --- a/ghost/core/test/legacy/api/admin/pages.test.js +++ b/ghost/core/test/legacy/api/admin/pages.test.js @@ -35,7 +35,9 @@ describe('Pages API', function () { .expect(200); }) .then((res) => { - assert.equal(res.body.pages[0].mobiledoc, '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"p",[[0,[],0,"HTML Ipsum presents"]]]]}'); + // ?source=html is converted to lexical + assert.equal(res.body.pages[0].mobiledoc, null); + assert.ok(res.body.pages[0].lexical.includes('HTML Ipsum presents')); }); }); }); diff --git a/ghost/core/test/legacy/models/model-posts.test.js b/ghost/core/test/legacy/models/model-posts.test.js index 8f8dcda0117..45a4df48122 100644 --- a/ghost/core/test/legacy/models/model-posts.test.js +++ b/ghost/core/test/legacy/models/model-posts.test.js @@ -625,9 +625,10 @@ describe('Post Model', function () { assert.equal(createdPost.has('uuid'), true); assert.equal(createdPost.get('status'), 'draft'); assert.equal(createdPost.get('title'), newPost.title, 'title is correct'); - assert.equal(createdPost.get('mobiledoc'), newPost.mobiledoc, 'mobiledoc is correct'); + assert.equal(createdPost.get('mobiledoc'), null, 'mobiledoc is converted to lexical'); + assert.ok(createdPost.get('lexical'), 'lexical is set'); assert.equal(createdPost.has('html'), true); - assert.equal(createdPost.get('html'), newPostDB.html); + assert.ok(createdPost.get('html')); assert.equal(createdPost.has('plaintext'), true); assert.match(createdPost.get('plaintext'), /^testing/); assert.equal(createdPost.get('slug'), newPostDB.slug + '-2'); @@ -680,7 +681,6 @@ describe('Post Model', function () { }); const newPost = testUtils.DataGenerator.forModel.posts[2]; - const newPostDB = testUtils.DataGenerator.Content.posts[2]; const addedPost = await models.Post.add(newPost, _.merge({withRelated: ['authors']}, context)); const createdPost = await models.Post.findOne({id: addedPost.id, status: 'all'}, {withRelated: ['authors']}); @@ -688,9 +688,10 @@ describe('Post Model', function () { assert.equal(createdPost.has('uuid'), true); assert.equal(createdPost.get('status'), 'draft'); assert.equal(createdPost.get('title'), newPost.title, 'title is correct'); - assert.equal(createdPost.get('mobiledoc'), newPost.mobiledoc, 'mobiledoc is correct'); + assert.equal(createdPost.get('mobiledoc'), null, 'mobiledoc is converted to lexical'); + assert.ok(createdPost.get('lexical'), 'lexical is set'); assert.equal(createdPost.has('html'), true); - assert.equal(createdPost.get('html'), newPostDB.html); + assert.ok(createdPost.get('html')); assert.equal(createdPost.has('plaintext'), true); assert.match(createdPost.get('plaintext'), /^testing/); // assert.equal(createdPost.get('slug'), newPostDB.slug + '-3'); @@ -877,7 +878,7 @@ describe('Post Model', function () { } assert.equal(post.get('slug'), 'test-title-' + num); - assert.equal(JSON.parse(post.get('mobiledoc')).cards[0][1].markdown, 'Test Content ' + num); + assert.ok(post.get('lexical').includes('Test Content ' + num)); assert.equal(Object.keys(eventsTriggered).length, 2); assertExists(eventsTriggered['post.added']); @@ -992,8 +993,10 @@ describe('Post Model', function () { }; const createdPost = await models.Post.add(post, context); - assert.equal(createdPost.get('mobiledoc'), `{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"${siteUrl}/content/images/card.jpg"}]],"markups":[["a",["href","${siteUrl}/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}`); - assert.equal(createdPost.get('html'), `

Testing

`); + // mobiledoc input is converted to lexical; urls are read back as absolute + assert.equal(createdPost.get('mobiledoc'), null); + assert.equal(createdPost.get('lexical'), `{"root":{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Testing","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","rel":null,"target":null,"title":null,"url":"${siteUrl}/test","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"type":"image","src":"${siteUrl}/content/images/card.jpg"}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`); + assert.equal(createdPost.get('html'), `

Testing

`); assert(createdPost.get('plaintext').includes('Testing')); assert.equal(createdPost.get('custom_excerpt'), `Testing links in custom excerpts`); assert.equal(createdPost.get('codeinjection_head'), ``); @@ -1018,8 +1021,9 @@ describe('Post Model', function () { const knexResult = await db.knex('posts').where({id: updatedPost.id}); const [knexPost] = knexResult; - assert.equal(knexPost.mobiledoc, '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"__GHOST_URL__/content/images/card.jpg"}]],"markups":[["a",["href","__GHOST_URL__/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}'); - assert.equal(knexPost.html, '

Testing

'); + assert.equal(knexPost.mobiledoc, null); + assert.equal(knexPost.lexical, '{"root":{"children":[{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Testing","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","rel":null,"target":null,"title":null,"url":"__GHOST_URL__/test","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1},{"type":"image","src":"__GHOST_URL__/content/images/card.jpg"}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}'); + assert.equal(knexPost.html, '

Testing

'); assert(knexPost.plaintext.includes('Testing')); assert.equal(knexPost.custom_excerpt, 'Testing links in custom excerpts'); assert.equal(knexPost.codeinjection_head, ''); @@ -1533,119 +1537,6 @@ describe('Post Model', function () { }); }); - describe('mobiledoc versioning', function () { - it('can create revisions', function () { - const newPost = { - mobiledoc: markdownToMobiledoc('a') - }; - - return models.Post.add(newPost, context) - .then((createdPost) => { - return models.Post.findOne({id: createdPost.id, status: 'all'}); - }) - .then((createdPost) => { - assertExists(createdPost); - - return createdPost.save({mobiledoc: markdownToMobiledoc('b')}, context); - }) - .then((updatedPost) => { - assert.equal(updatedPost.get('mobiledoc'), markdownToMobiledoc('b')); - - return models.MobiledocRevision - .findAll({ - filter: `post_id:'${updatedPost.id}'` - }); - }) - .then((mobiledocRevisions) => { - assert.equal(mobiledocRevisions.length, 2); - - assert.equal(mobiledocRevisions.toJSON()[0].mobiledoc, markdownToMobiledoc('b')); - assert.equal(mobiledocRevisions.toJSON()[1].mobiledoc, markdownToMobiledoc('a')); - }); - }); - - it('keeps only 10 last revisions in FIFO style', function () { - let revisionedPost; - const newPost = { - mobiledoc: markdownToMobiledoc('revision: 0') - }; - - return models.Post.add(newPost, context) - .then((createdPost) => { - return models.Post.findOne({id: createdPost.id, status: 'all'}); - }) - .then((createdPost) => { - assertExists(createdPost); - revisionedPost = createdPost; - - return sequence(_.times(11, (i) => { - return () => { - return models.Post.edit({ - mobiledoc: markdownToMobiledoc('revision: ' + (i + 1)) - }, _.extend({}, context, {id: createdPost.id})); - }; - })); - }) - .then(() => models.MobiledocRevision - .findAll({ - filter: `post_id:'${revisionedPost.id}'` - }) - ) - .then((mobiledocRevisions) => { - assert.equal(mobiledocRevisions.length, 10); - - assert.equal(mobiledocRevisions.toJSON()[0].mobiledoc, markdownToMobiledoc('revision: 11')); - assert.equal(mobiledocRevisions.toJSON()[9].mobiledoc, markdownToMobiledoc('revision: 2')); - }); - }); - - it('creates 2 revisions after first edit for previously unversioned post', function () { - let unversionedPost; - - const newPost = { - title: 'post title', - mobiledoc: markdownToMobiledoc('a') - }; - - // passing 'migrating' flag to simulate unversioned post - const options = Object.assign(_.clone(context), {migrating: true}); - - return models.Post.add(newPost, options) - .then((createdPost) => { - assertExists(createdPost); - unversionedPost = createdPost; - assert.equal(createdPost.get('mobiledoc'), markdownToMobiledoc('a')); - - return models.MobiledocRevision - .findAll({ - filter: `post_id:'${createdPost.id}'` - }); - }) - .then((mobiledocRevisions) => { - assert.equal(mobiledocRevisions.length, 0); - - return models.Post.edit({ - mobiledoc: markdownToMobiledoc('b') - }, _.extend({}, context, {id: unversionedPost.id})); - }) - .then((editedPost) => { - assertExists(editedPost); - assert.equal(editedPost.get('mobiledoc'), markdownToMobiledoc('b')); - - return models.MobiledocRevision - .findAll({ - filter: `post_id:'${editedPost.id}'` - }); - }) - .then((mobiledocRevisions) => { - assert.equal(mobiledocRevisions.length, 2); - - assert.equal(mobiledocRevisions.toJSON()[0].mobiledoc, markdownToMobiledoc('b')); - assert.equal(mobiledocRevisions.toJSON()[1].mobiledoc, markdownToMobiledoc('a')); - }); - }); - }); - describe('Multiauthor Posts', function () { beforeAll(testUtils.teardownDb); diff --git a/ghost/core/test/unit/api/canary/utils/serializers/input/pages.test.js b/ghost/core/test/unit/api/canary/utils/serializers/input/pages.test.js index d6922be3aef..76504acd4b7 100644 --- a/ghost/core/test/unit/api/canary/utils/serializers/input/pages.test.js +++ b/ghost/core/test/unit/api/canary/utils/serializers/input/pages.test.js @@ -3,7 +3,7 @@ const sinon = require('sinon'); const serializers = require('../../../../../../../core/server/api/endpoints/utils/serializers'); const postsSchema = require('../../../../../../../core/server/data/schema').tables.posts; -const mobiledocLib = require('../../../../../../../core/server/lib/mobiledoc'); +const lexicalLib = require('../../../../../../../core/server/lib/lexical'); describe('Unit: endpoints/utils/serializers/input/pages', function () { afterEach(function () { @@ -262,13 +262,13 @@ describe('Unit: endpoints/utils/serializers/input/pages', function () { } }; - sinon.stub(mobiledocLib, 'htmlToMobiledocConverter').get(() => () => { + sinon.stub(lexicalLib, 'htmlToLexicalConverter').get(() => () => { throw new Error('Some error'); }); assert.throws(() => { serializers.input.posts.edit({}, frame); - }, /Failed to convert HTML to Mobiledoc/); + }, /Failed to convert HTML to Lexical/); }); describe('Ensure relations format', function () { diff --git a/ghost/core/test/unit/api/canary/utils/serializers/input/posts.test.js b/ghost/core/test/unit/api/canary/utils/serializers/input/posts.test.js index 364578ed71d..6be90c3a984 100644 --- a/ghost/core/test/unit/api/canary/utils/serializers/input/posts.test.js +++ b/ghost/core/test/unit/api/canary/utils/serializers/input/posts.test.js @@ -4,7 +4,7 @@ const serializers = require('../../../../../../../core/server/api/endpoints/util const urlService = require('../../../../../../../core/server/services/url'); const postsSchema = require('../../../../../../../core/server/data/schema').tables.posts; -const mobiledocLib = require('../../../../../../../core/server/lib/mobiledoc'); +const lexicalLib = require('../../../../../../../core/server/lib/lexical'); describe('Unit: endpoints/utils/serializers/input/posts', function () { afterEach(function () { @@ -428,10 +428,8 @@ describe('Unit: endpoints/utils/serializers/input/posts', function () { let postData = frame.data.posts[0]; assert.notEqual(postData.lexical, lexical); assert.equal(postData.lexical, '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"this is great feature","type":"extended-text","version":1}],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'); - // we convert to both mobiledoc and lexical to avoid changing formats - // for existing content when updating with `?source=html, - // the unused data is cleared in the Post model when saving - assert.equal(postData.mobiledoc, '{"version":"0.3.1","atoms":[],"cards":[],"markups":[],"sections":[[1,"p",[[0,[],0,"this is great feature"]]]]}'); + // `?source=html` only ever produces lexical now + assert.equal(postData.mobiledoc, undefined); }); // JSDOM require is sometimes very slow on CI causing random timeouts @@ -473,13 +471,13 @@ describe('Unit: endpoints/utils/serializers/input/posts', function () { } }; - sinon.stub(mobiledocLib, 'htmlToMobiledocConverter').get(() => () => { + sinon.stub(lexicalLib, 'htmlToLexicalConverter').get(() => () => { throw new Error('Some error'); }); assert.throws(() => { serializers.input.posts.edit({}, frame); - }, /Failed to convert HTML to Mobiledoc/); + }, /Failed to convert HTML to Lexical/); }); }); diff --git a/ghost/core/test/unit/server/lib/mobiledoc.test.js b/ghost/core/test/unit/server/lib/mobiledoc.test.js deleted file mode 100644 index aaa3a0613cc..00000000000 --- a/ghost/core/test/unit/server/lib/mobiledoc.test.js +++ /dev/null @@ -1,399 +0,0 @@ -const assert = require('node:assert/strict'); -const {assertExists} = require('../../../utils/assertions'); -const path = require('path'); -const sinon = require('sinon'); -const nock = require('nock'); -const configUtils = require('../../../utils/config-utils'); -const mobiledocLib = require('../../../../core/server/lib/mobiledoc'); -const storage = require('../../../../core/server/adapters/storage'); -const urlUtils = require('../../../../core/shared/url-utils'); -const mockUtils = require('../../../utils/mocks'); - -describe('lib/mobiledoc', function () { - afterEach(async function () { - sinon.restore(); - nock.cleanAll(); - await configUtils.restore(); - // ensure config changes are reset and picked up by next test - mobiledocLib.reload(); - mockUtils.modules.unmockNonExistentModule(/sharp/); - }); - - describe('mobiledocHtmlRenderer', function () { - it('renders all default cards and atoms', function () { - let mobiledoc = { - version: '0.3.1', - ghostVersion: '0.3', - atoms: [ - ['soft-return', '', {}] - ], - cards: [ - ['markdown', { - markdown: '# Markdown card\nSome markdown' - }], - ['paywall', {}], - ['hr', {}], - ['image', { - cardWidth: 'wide', - src: '/content/images/2018/04/NatGeo06.jpg', - width: 4000, - height: 2000, - caption: 'Birdies' - }], - ['html', { - html: '

HTML card

\n

Some HTML

' - }], - ['embed', { - html: '

Embed card

' - }], - ['gallery', { - images: [{ - row: 0, - fileName: 'test.png', - src: '/content/images/test.png', - width: 1000, - height: 500 - }] - }] - ], - markups: [], - sections: [ - [1, 'p', [ - [0, [], 0, 'One'], - [1, [], 0, 0], - [0, [], 0, 'Two'] - ]], - [10, 0], - [1, 'p', [ - [0, [], 0, 'Three'] - ]], - [10, 1], - [10, 2], - [10, 3], - [1, 'p', [ - [0, [], 0, 'Four'] - ]], - [10, 4], - [10, 5], - [10, 6], - [1, 'p', []] - ] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '

One
Two

Markdown card

\n

Some markdown

\n

Three


Birdies

Four

HTML card

\n

Some HTML

Embed card

'); - }); - - it('renders according to ghostVersion', function () { - let mobiledoc = { - version: '0.3.1', - ghostVersion: '4.0', - atoms: [], - cards: [ - ['markdown', { - markdown: '# Header One' - }] - ], - markups: [], - sections: [ - [10, 0], - [1, 'h2', [ - [0, [], 0, 'Héader Two']] - ] - ] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '

Header One

\n

Héader Two

'); - }); - - it('renders srcsets for __GHOST_URL__ relative images', function () { - let mobiledoc = { - version: '0.3.1', - atoms: [], - cards: [ - ['image', { - cardWidth: 'wide', - src: '__GHOST_URL__/content/images/2018/04/NatGeo06.jpg', - width: 4000, - height: 2000, - caption: 'Birdies' - }], - ['gallery', { - images: [{ - row: 0, - fileName: 'test.png', - src: '__GHOST_URL__/content/images/test.png', - width: 1000, - height: 500 - }] - }] - ], - markups: [], - sections: [ - [10, 0], - [10, 1] - ] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '
Birdies
'); - }); - - it('renders srcsets for absolute images', function () { - const siteUrl = configUtils.config.get('url'); - let mobiledoc = { - version: '0.3.1', - atoms: [], - cards: [ - ['image', { - cardWidth: 'wide', - src: `${siteUrl}/content/images/2018/04/NatGeo06.jpg`, - width: 4000, - height: 2000, - caption: 'Birdies' - }], - ['gallery', { - images: [{ - row: 0, - fileName: 'test.png', - src: `${siteUrl}/content/images/test.png`, - width: 1000, - height: 500 - }] - }] - ], - markups: [], - sections: [ - [10, 0], - [10, 1] - ] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), `
Birdies
`); - }); - - it('respects srcsets config', function () { - configUtils.set('imageOptimization:srcsets', false); - - let mobiledoc = { - version: '0.3.1', - atoms: [], - cards: [ - ['image', { - cardWidth: 'wide', - src: '/content/images/2018/04/NatGeo06.jpg', - width: 4000, - height: 2000, - caption: 'Birdies' - }], - ['gallery', { - images: [{ - row: 0, - fileName: 'test.png', - src: '/content/images/test.png', - width: 1000, - height: 500 - }] - }] - ], - markups: [], - sections: [ - [10, 0], - [10, 1] - ] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '
Birdies
'); - }); - - it('does render srcsets for animated images', function () { - let mobiledoc = { - version: '0.3.1', - atoms: [], - cards: [ - ['image', { - cardWidth: '', - src: '/content/images/2020/07/animated.gif', - width: 4000, - height: 2000 - }] - ], - markups: [], - sections: [[10, 0]] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '
'); - }); - - it('does not render srcsets for non-resizable images', function () { - let mobiledoc = { - version: '0.3.1', - atoms: [], - cards: [ - ['image', { - cardWidth: '', - src: '/content/images/2020/07/vector.svg', - width: 4000, - height: 2000 - }] - ], - markups: [], - sections: [[10, 0]] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '
'); - }); - - it('does not render srcsets when sharp is not available', function () { - mockUtils.modules.mockNonExistentModule('sharp', new Error(), true); - - let mobiledoc = { - version: '0.3.1', - atoms: [], - cards: [ - ['image', { - src: '/content/images/2018/04/NatGeo06.jpg', - width: 4000, - height: 2000 - }] - ], - markups: [], - sections: [ - [10, 0] - ] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '
'); - }); - - it('does not render srcsets with incompatible storage engine', function () { - sinon.stub(storage.getStorage('images'), 'saveRaw').value(null); - - let mobiledoc = { - version: '0.3.1', - atoms: [], - cards: [ - ['image', { - src: '/content/images/2018/04/NatGeo06.jpg', - width: 4000, - height: 2000 - }] - ], - markups: [], - sections: [ - [10, 0] - ] - }; - - assert.equal(mobiledocLib.mobiledocHtmlRenderer.render(mobiledoc), '
'); - }); - }); - - describe('populateImageSizes', function () { - let originalStoragePath; - - beforeEach(function () { - originalStoragePath = storage.getStorage().storagePath; - storage.getStorage('images').storagePath = path.join(__dirname, '../../../utils/fixtures/images/'); - }); - - afterEach(function () { - storage.getStorage('images').storagePath = originalStoragePath; - }); - - it('works', async function () { - let mobiledoc = { - cards: [ - ['image', {src: '/content/images/ghost-logo.png'}], - ['image', {src: 'http://example.com/external.jpg'}], - ['image', {src: 'https://images.unsplash.com/favicon_too_large?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ'}], - ['image', {}] - ] - }; - - const unsplashMock = nock('https://images.unsplash.com/') - .get('/favicon_too_large') - .query(true) - .replyWithFile(200, path.join(__dirname, '../../../utils/fixtures/images/favicon_not_square.png'), { - 'Content-Type': 'image/png' - }); - - const transformedMobiledoc = await mobiledocLib.populateImageSizes(JSON.stringify(mobiledoc)); - const transformed = JSON.parse(transformedMobiledoc); - - assert.equal(unsplashMock.isDone(), true); - - assert.equal(transformed.cards.length, 4); - // external image should not be fetched (no sizing attempted) - assert.equal(transformed.cards[1][1].width, undefined); - }); - - it('fetches non-local image sizes from URL when urls:image is configured', async function () { - configUtils.set('urls:image', 'https://storage.ghost.is/c/6f/a3/test/content/images'); - mobiledocLib.reload(); - - let mobiledoc = { - cards: [ - ['image', {src: 'https://storage.ghost.is/c/6f/a3/test/content/images/2026/02/ghost-logo.png'}] - ] - }; - - const cdnMock = nock('https://storage.ghost.is') - .get('/c/6f/a3/test/content/images/2026/02/ghost-logo.png') - .query(true) - .replyWithFile(200, path.join(__dirname, '../../../utils/fixtures/images/ghost-logo.png'), { - 'Content-Type': 'image/png' - }); - - const transformedMobiledoc = await mobiledocLib.populateImageSizes(JSON.stringify(mobiledoc)); - const transformed = JSON.parse(transformedMobiledoc); - - assert.equal(cdnMock.isDone(), true); - assert.equal(transformed.cards.length, 1); - assert.equal(transformed.cards[0][1].width, 800); - assert.equal(transformed.cards[0][1].height, 257); - }); - - it('skips sizing for arbitrary external URLs', async function () { - let mobiledoc = { - cards: [ - ['image', {src: 'http://169.254.169.254/latest/meta-data/'}] - ] - }; - - const transformedMobiledoc = await mobiledocLib.populateImageSizes(JSON.stringify(mobiledoc)); - const transformed = JSON.parse(transformedMobiledoc); - - assert.equal(transformed.cards[0][1].width, undefined); - assert.equal(transformed.cards[0][1].height, undefined); - }); - - // images can be stored with and without subdir when a subdir is configured - // but storage adapter always needs paths relative to content dir - it('works with subdir', async function () { - // urlUtils is a class instance and won't pick up changes to config so - // it's necessary to stub out the internals used by - sinon.stub(urlUtils, 'getSubdir').returns('/subdir'); - - let mobiledoc = { - cards: [ - ['image', {src: '/content/images/ghost-logo.png'}], - ['image', {src: '/subdir/content/images/ghost-logo.png'}] - ] - }; - - const transformedMobiledoc = await mobiledocLib.populateImageSizes(JSON.stringify(mobiledoc)); - const transformed = JSON.parse(transformedMobiledoc); - - assert.equal(transformed.cards.length, 2); - - assertExists(transformed.cards[0][1].width); - assert.equal(transformed.cards[0][1].width, 800); - assertExists(transformed.cards[0][1].height); - assert.equal(transformed.cards[0][1].height, 257); - - assertExists(transformed.cards[1][1].width); - assert.equal(transformed.cards[1][1].width, 800); - assertExists(transformed.cards[1][1].height); - assert.equal(transformed.cards[1][1].height, 257); - }); - }); -}); diff --git a/ghost/core/test/utils/batch-email-utils.js b/ghost/core/test/utils/batch-email-utils.js index 4b290ebef21..1364b966a0f 100644 --- a/ghost/core/test/utils/batch-email-utils.js +++ b/ghost/core/test/utils/batch-email-utils.js @@ -70,7 +70,8 @@ async function sendEmail(agent, settings, email_recipient_filter) { assert.ok(emailModel.get('subject')); assert.ok(emailModel.get('from')); - assert.equal(emailModel.get('source_type'), settings && settings.mobiledoc ? 'mobiledoc' : 'lexical'); + // posts created with mobiledoc are converted to lexical on save + assert.equal(emailModel.get('source_type'), 'lexical'); // Await sending job await completedPromise; @@ -95,7 +96,8 @@ async function sendFailedEmail(agent, settings, email_recipient_filter) { assert.ok(emailModel.get('subject')); assert.ok(emailModel.get('from')); - assert.equal(emailModel.get('source_type'), settings && settings.mobiledoc ? 'mobiledoc' : 'lexical'); + // posts created with mobiledoc are converted to lexical on save + assert.equal(emailModel.get('source_type'), 'lexical'); // Await sending job await completedPromise; diff --git a/ghost/core/test/utils/fixture-utils.js b/ghost/core/test/utils/fixture-utils.js index 491beeda73b..f66c3383bca 100644 --- a/ghost/core/test/utils/fixture-utils.js +++ b/ghost/core/test/utils/fixture-utils.js @@ -60,9 +60,32 @@ const fixtures = { return Promise.all(DataGenerator.forKnex.posts_meta.map((postMeta) => { return models.PostsMeta.add(postMeta, context.internal); })); + }) + .then(function () { + return fixtures.restoreLegacyMobiledocPosts(); }); }, + // New saves convert mobiledoc to lexical, but some fixtures need to remain + // stored as mobiledoc to exercise the legacy mobiledoc read/render paths. + // Restore the original mobiledoc directly in the DB to simulate a legacy row + // (the html generated during insert is kept and still contains the same URLs). + restoreLegacyMobiledocPosts: function restoreLegacyMobiledocPosts() { + const legacySlugs = ['post-with-all-media-types-mobiledoc']; + + return Promise.all(legacySlugs.map((slug) => { + const fixture = DataGenerator.forKnex.posts.find(post => post.slug === slug); + + if (!fixture) { + return Promise.resolve(); + } + + return models.Base.knex('posts') + .where('id', fixture.id) + .update({mobiledoc: fixture.mobiledoc, lexical: null}); + })); + }, + insertMultiAuthorPosts: function insertMultiAuthorPosts() { // Creates two posts per staff member. // (It used to create 10, but then other tests assumed two per staff member) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 878da68caf7..8b64ef82dca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2604,9 +2604,6 @@ importers: '@tryghost/kg-converters': specifier: 'catalog:' version: 1.2.2 - '@tryghost/kg-default-atoms': - specifier: 5.2.2 - version: 5.2.2 '@tryghost/kg-default-cards': specifier: 10.3.2 version: 10.3.2(encoding@0.1.13) @@ -2622,9 +2619,6 @@ importers: '@tryghost/kg-markdown-html-renderer': specifier: 7.2.2 version: 7.2.2 - '@tryghost/kg-mobiledoc-html-renderer': - specifier: 7.2.2 - version: 7.2.2 '@tryghost/limit-service': specifier: 'catalog:' version: 1.5.6 @@ -2819,7 +2813,7 @@ importers: version: 13.0.0 gscan: specifier: 6.4.0 - version: 6.4.0 + version: 6.4.0(supports-color@5.5.0) handlebars: specifier: 4.7.9 version: 4.7.9 @@ -3179,9 +3173,6 @@ importers: specifier: 'catalog:' version: 4.1.8(@opentelemetry/api@1.9.1)(@types/node@22.19.21)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.6(@types/node@22.19.21)(typescript@5.9.3))(vite@7.3.2(@types/node@22.19.21)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) optionalDependencies: - '@tryghost/html-to-mobiledoc': - specifier: 3.3.2 - version: 3.3.2(@noble/hashes@1.8.0) sqlite3: specifier: 5.1.7 version: 5.1.7 @@ -9009,9 +9000,6 @@ packages: '@tryghost/helpers@1.1.106': resolution: {integrity: sha512-Yjrb/VLvRqgtMsiKXvuSRoVvAm2HJ6SJakdo25va4sU9sA7yXBSWWHkukE8q27Ty9R2fE5jEEWziIhrZy+Tkog==} - '@tryghost/html-to-mobiledoc@3.3.2': - resolution: {integrity: sha512-fCf4aHPV7hmF0AugRMfADiFJRh2ZxlhaQsUQSm0iVV5Kof7PgRIt8V7AYphV++aVLm8sXCU8rQkcDr5VMm9T+Q==} - '@tryghost/html-to-plaintext@1.0.11': resolution: {integrity: sha512-pCmCQLrZN+IqTUli07I+1oZGkZN4a6PDU/lqJ2DTe6Up86OmPOKePWUH43zzOGM5RO3aaRP1I3zUzfOW1iCFxA==} @@ -9041,10 +9029,6 @@ packages: '@tryghost/kg-converters@1.2.2': resolution: {integrity: sha512-15wigkPDdFkfWZx2uJFHhgdVItGesjzAaM11GTC/3lmqaqjZrcohvMIvI1BhW2HOcid4PeUnlD9eox/6Isai/w==} - '@tryghost/kg-default-atoms@5.2.2': - resolution: {integrity: sha512-ZWNKaUjEyAcjyltL4k6vyBh3O2cmST15Bx7jRADeOBVQm5ScAgNyWI7sIaJvELhrb8X7mfp909pAcqH/ZGDauA==} - engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-default-cards@10.3.2': resolution: {integrity: sha512-A9Jymh2VFtzqWb6/m8a6pHNLSflDwvVcX159v6vvbcwFV+/BhCHk138n9c/2bcMS8tUPCvHuI+LiFwuohtJaBQ==} engines: {node: ^22.13.1 || ^24.0.0} @@ -9066,14 +9050,6 @@ packages: resolution: {integrity: sha512-k2b/Eu8X7DHlP4T45IjWvwTMIH7DFmkGjifCwJAe6+JCpsC7FzAGp5Yo50mSvttn/AK9NKbw3UScG4GR8TfNWg==} engines: {node: ^22.13.1 || ^24.0.0} - '@tryghost/kg-mobiledoc-html-renderer@7.2.2': - resolution: {integrity: sha512-616EF/4ODqlDxcYxpvqtm/y62RiP727kxcLLC/UtqE5KwwPTL1Dt870YfFSA4DqehDZE1/avB4VhLv6+kcUa9g==} - engines: {node: ^22.13.1 || ^24.0.0} - - '@tryghost/kg-parser-plugins@4.3.2': - resolution: {integrity: sha512-VWODmGYeriEPeAD0gvJsXmc7RtISoWh2L+eQVq0TjhFQkiNGlX7ocXbcDXEZkbqBwKcZUtDPOqcI6h+NQUPjPA==} - engines: {node: ^22.13.1} - '@tryghost/kg-unsplash-selector@0.4.2': resolution: {integrity: sha512-1m4NbxCi5VHN2d6JlxWXKj3gy/rRzFez5ZD9Y5RdOovZgQhLKvCz2PiEYwRqRpLgaINxSYH3MTF3Q24jWkGvJQ==} peerDependencies: @@ -9098,9 +9074,6 @@ packages: '@tryghost/metrics@1.0.43': resolution: {integrity: sha512-zTpO/VWhhsDgT/NPeXTa2UNO6KRoMVroo5oHpvKCB29eSE2td6uSarxZCzoxZ9c6rgA2TW0tiGuNu7sB3bMY7g==} - '@tryghost/mobiledoc-kit@0.12.4-ghost.1': - resolution: {integrity: sha512-c4aheSWH2Y7x4uSkAx08gbtvuEgPGjlu6v+FeUdSJZ1blEd+knL3zTcUAfeSiM6rgLEHxlNWtt+KFwotdf6rTA==} - '@tryghost/mongo-knex@0.10.1': resolution: {integrity: sha512-6LRA4zVpNvCrVrA9hOhs8N4iylAiafGssiKuD8yML/13xg2PvKw6dzOZj6hg0CKQwG7tVp8TA+7Nw3iDOzy/jQ==} @@ -17520,15 +17493,6 @@ packages: mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mobiledoc-dom-renderer@0.7.0: - resolution: {integrity: sha512-A+gT6D4Ru3DKY7ZYOBRORmwhRJ7rDj2vy75D2dWuZS5NgX0mCmGs0yN7qs48YlxvfCif8RFpYsaaPg6Kc3MdJg==} - - mobiledoc-dom-renderer@0.7.2: - resolution: {integrity: sha512-0vw/ybxCWXI0sIcBk9GVq3PMfVWJ4qpLWBal8ZoZVP/S5MMRjxFwyOctOfUmsY2dVi6BSvomp8yFqNloawwyig==} - - mobiledoc-text-renderer@0.4.0: - resolution: {integrity: sha512-+Tzfo0hhUFxS0n5FWZ0nf6WUrvnVmsxaIdq0CyeLYD1lk8oW2ml+6WLdeLlzKM5OYYi3PWV6NR9HCUG01cdvWQ==} - mocha@2.5.3: resolution: {integrity: sha512-jNt2iEk9FPmZLzL+sm4FNyOIDYXf2wUU6L4Cc8OIKK/kzgMHKPi4YhTZqG4bW4kQVdIv6wutDybRhXfdnujA1Q==} engines: {node: '>= 0.8.x'} @@ -25831,12 +25795,12 @@ snapshots: '@opentelemetry/api': 1.9.1 '@opentelemetry/semantic-conventions': 1.41.1 - '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/api-logs': 0.214.0 import-in-the-middle: 3.0.1 - require-in-the-middle: 8.0.1 + require-in-the-middle: 8.0.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -27011,7 +26975,7 @@ snapshots: '@sentry/utils': 7.120.4 localforage: 1.10.0 - '@sentry/node-core@10.58.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': + '@sentry/node-core@10.58.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)(supports-color@5.5.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': dependencies: '@sentry/core': 10.58.0 '@sentry/opentelemetry': 10.58.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) @@ -27019,19 +26983,19 @@ snapshots: optionalDependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1)(supports-color@5.5.0) '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.41.1 - '@sentry/node@10.58.0': + '@sentry/node@10.58.0(supports-color@5.5.0)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1)(supports-color@5.5.0) '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) '@opentelemetry/semantic-conventions': 1.41.1 '@sentry/core': 10.58.0 - '@sentry/node-core': 10.58.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) + '@sentry/node-core': 10.58.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)(supports-color@5.5.0))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) '@sentry/opentelemetry': 10.58.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) '@sentry/server-utils': 10.58.0 import-in-the-middle: 3.0.1 @@ -28855,7 +28819,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tryghost/debug@2.3.0': + '@tryghost/debug@2.3.0(supports-color@5.5.0)': dependencies: '@tryghost/root-utils': 2.3.0 debug: 4.4.3(supports-color@5.5.0) @@ -28925,16 +28889,6 @@ snapshots: dependencies: lodash-es: 4.18.1 - '@tryghost/html-to-mobiledoc@3.3.2(@noble/hashes@1.8.0)': - dependencies: - '@tryghost/kg-parser-plugins': 4.3.2 - '@tryghost/mobiledoc-kit': 0.12.4-ghost.1 - jsdom: 29.1.1(@noble/hashes@1.8.0) - transitivePeerDependencies: - - '@noble/hashes' - - canvas - optional: true - '@tryghost/html-to-plaintext@1.0.11': dependencies: html-to-text: 8.2.1 @@ -28989,8 +28943,6 @@ snapshots: dependencies: lodash: 4.18.1 - '@tryghost/kg-default-atoms@5.2.2': {} - '@tryghost/kg-default-cards@10.3.2(encoding@0.1.13)': dependencies: '@tryghost/kg-markdown-html-renderer': 7.2.2 @@ -29082,17 +29034,6 @@ snapshots: markdown-it-sup: 2.0.0 semver: 7.8.1 - '@tryghost/kg-mobiledoc-html-renderer@7.2.2': - dependencies: - '@tryghost/kg-utils': 1.1.2 - mobiledoc-dom-renderer: 0.7.2 - simple-dom: 1.4.0 - - '@tryghost/kg-parser-plugins@4.3.2': - dependencies: - '@tryghost/kg-clean-basic-html': 4.3.2 - optional: true - '@tryghost/kg-unsplash-selector@0.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: react: 18.3.1 @@ -29145,12 +29086,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@tryghost/mobiledoc-kit@0.12.4-ghost.1': - dependencies: - mobiledoc-dom-renderer: 0.7.0 - mobiledoc-text-renderer: 0.4.0 - optional: true - '@tryghost/mongo-knex@0.10.1(supports-color@5.5.0)': dependencies: debug: 4.4.3(supports-color@5.5.0) @@ -29351,7 +29286,7 @@ snapshots: '@tryghost/server@3.1.0': dependencies: - '@tryghost/debug': 2.3.0 + '@tryghost/debug': 2.3.0(supports-color@5.5.0) '@tryghost/logging': 4.2.1(supports-color@5.5.0) transitivePeerDependencies: - '@75lb/nature' @@ -32069,7 +32004,7 @@ snapshots: transitivePeerDependencies: - supports-color - body-parser@2.2.2: + body-parser@2.2.2(supports-color@5.5.0): dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -37333,10 +37268,10 @@ snapshots: transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.2.1(supports-color@5.5.0): dependencies: accepts: 2.0.0 - body-parser: 2.2.2 + body-parser: 2.2.2(supports-color@5.5.0) content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 @@ -37346,7 +37281,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@5.5.0) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -37357,8 +37292,8 @@ snapshots: proxy-addr: 2.0.7 qs: 6.15.2 range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 + router: 2.2.0(supports-color@5.5.0) + send: 1.2.1(supports-color@5.5.0) serve-static: 2.2.1 statuses: 2.0.2 type-is: 2.1.0 @@ -37566,7 +37501,7 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@2.1.1: + finalhandler@2.1.1(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 @@ -38279,11 +38214,11 @@ snapshots: growly@1.3.0: {} - gscan@6.4.0: + gscan@6.4.0(supports-color@5.5.0): dependencies: - '@sentry/node': 10.58.0 + '@sentry/node': 10.58.0(supports-color@5.5.0) '@tryghost/config': 2.3.0 - '@tryghost/debug': 2.3.0 + '@tryghost/debug': 2.3.0(supports-color@5.5.0) '@tryghost/errors': 1.3.13 '@tryghost/logging': 4.2.1(supports-color@5.5.0) '@tryghost/nql': 0.13.1(supports-color@5.5.0) @@ -38291,7 +38226,7 @@ snapshots: '@tryghost/server': 3.1.0 '@tryghost/zip': 3.4.0 chalk: 5.6.2 - express: 5.2.1 + express: 5.2.1(supports-color@5.5.0) express-handlebars: 8.0.1 glob: 13.0.6 handlebars: 4.7.9 @@ -41572,14 +41507,6 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 - mobiledoc-dom-renderer@0.7.0: - optional: true - - mobiledoc-dom-renderer@0.7.2: {} - - mobiledoc-text-renderer@0.4.0: - optional: true - mocha@2.5.3: dependencies: commander: 2.3.0 @@ -44452,7 +44379,7 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@8.0.1: + require-in-the-middle@8.0.1(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) module-details-from-path: 1.0.4 @@ -44673,7 +44600,7 @@ snapshots: route-recognizer@0.3.4: {} - router@2.2.0: + router@2.2.0(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 @@ -44881,7 +44808,7 @@ snapshots: transitivePeerDependencies: - supports-color - send@1.2.1: + send@1.2.1(supports-color@5.5.0): dependencies: debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 @@ -44924,7 +44851,7 @@ snapshots: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@5.5.0) transitivePeerDependencies: - supports-color