/** * # Spec * * This file contains details of the theme API spec, in a format that can be used by GScan */ const oneLineTrim = require('common-tags/lib/oneLineTrim'); const docsBaseUrl = `https://ghost.org/docs/themes/`; let knownHelpers, templates, rules, ruleNext; // eslint-disable-line no-unused-vars knownHelpers = [ // Ghost 'foreach', 'has', 'is', 'get', 'content', 'excerpt', 'title', 'tags', 'author', 'authors', 'img_url', 'navigation', 'pagination', 'page_url', 'url', 'date', 'plural', 'encode', 'asset', 'body_class', 'post_class', 'ghost_head', 'ghost_foot', 'lang', 'meta_title', 'meta_description', 'next_post', 'prev_post', 't', 'twitter_url', 'facebook_url', 'reading_time', // Ghost apps 'input_email', 'input_password', 'amp_components', 'amp_content', 'amp_ghost_head', 'subscribe_form', // Handlebars and express handlebars 'log', 'if', 'unless', 'with', 'block', 'contentFor', 'each', 'lookup' // Registering these will break template compile checks // 'blockHelperMissing', 'helperMissing', ]; templates = [ { name: 'Page template', pattern: /^page\.hbs$/, version: '>=0.4.0' }, { name: 'Error template', pattern: /^error\.hbs$/, version: '>=0.4.0' }, { name: 'Tag template', pattern: /^tag\.hbs$/, version: '>=0.4.2' }, { name: 'Custom page template', pattern: /^page-([a-z0-9\-_]+)\.hbs$/, version: '>=0.4.2' }, { name: 'Author template', pattern: /^author\.hbs$/, version: '>=0.5.0' }, { name: 'Home template', pattern: /^home\.hbs$/, version: '>=0.5.0' }, { name: 'Custom tag template', pattern: /^tag-([a-z0-9\-_]+)\.hbs$/, version: '>=0.5.0' }, { name: 'Custom author template', pattern: /^author-([a-z0-9\-_]+)\.hbs$/, version: '>=0.6.3' }, { name: 'Private template', pattern: /^private\.hbs$/, version: '>=0.6.3' }, { name: 'Custom post template', pattern: /^post-([a-z0-9\-_]+)\.hbs$/, version: '>=0.7.3' } ]; rules = { 'GS001-DEPR-PURL': { level: 'error', rule: 'Replace {{pageUrl}} with {{page_url}}', fatal: true, details: oneLineTrim`The helper {{pageUrl}} was replaced with {{page_url}}.
Find more information about the {{page_url}} helper here.`, regex: /{{\s*?pageUrl\b[\w\s='"]*?}}/ig, helper: '{{pageUrl}}' }, 'GS001-DEPR-MD': { level: 'error', rule: 'The usage of {{meta_description}} in HTML head is no longer required', details: oneLineTrim`The usage of {{meta_description}} in the HTML head tag is no longer required because Ghost outputs this for you automatically in {{ghost_head}}.
Check out the documentation for {{meta_description}} here.
To see, what else is rendered with the {{ghost_head}} helper, look here.`, regex: /{{image}} helper was replaced with the {{img_url}} helper..', fatal: true, details: oneLineTrim`The {{image}} helper was replaced with the {{img_url}} helper.
Depending on the context of the {{img_url}} helper you would need to use e. g.

{{#post}}
    {{img_url feature_image}}
{{/post}}


to render the feature image of the blog post.

If you are using {{if image}}, then you have to replace it with e.g. {{if feature_image}}.

Find more information about the {{img_url}} helper here and read more about Ghost's usage of contexts here.`, regex: /{{\s*?image\b[\w\s='"]*?}}/g, helper: '{{image}}' }, 'GS001-DEPR-COV': { level: 'error', rule: 'Replace {{cover}} with {{cover_image}}', fatal: true, details: oneLineTrim`The cover attribute was replaced with cover_image. To render the cover image in author context, you need to use

{{#author}}
    {{cover_image}}
{{/author}}


See the object attributes of author here.
To render the cover image of your blog, just use {{@site.cover_image}}. See here.`, regex: /{{\s*?cover\s*?}}/g, helper: '{{cover}}' }, 'GS001-DEPR-AIMG': { level: 'error', rule: 'Replace {{author.image}} with {{author.profile_image}}', fatal: true, details: oneLineTrim`The image attribute in author context was replaced with profile_image.
Instead of {{author.image}} you need to use {{author.profile_image}}.
See the object attributes of author here.`, regex: /{{\s*?author\.image\s*?}}/g, helper: '{{author.image}}' }, 'GS001-DEPR-PIMG': { level: 'error', rule: 'Replace {{post.image}} with {{post.feature_image}}', fatal: true, details: oneLineTrim`The image attribute in post context was replaced with feature_image.
Instead of {{post.image}} you need to use {{post.feature_image}}.
See the object attributes of post here.`, regex: /{{\s*?post\.image\s*?}}/g, helper: '{{post.image}}' }, 'GS001-DEPR-BC': { level: 'error', rule: 'Replace {{@blog.cover}} with {{@site.cover_image}}', fatal: true, details: oneLineTrim`The cover attribute was replaced with cover_image.
Instead of {{@blog.cover}} you need to use {{@site.cover_image}}.
See here.`, regex: /{{\s*?@blog\.cover\s*?}}/g, helper: '{{@blog.cover}}' }, 'GS001-DEPR-AC': { level: 'error', rule: 'Replace {{author.cover}} with {{author.cover_image}}', fatal: true, details: oneLineTrim`The cover attribute was replaced with cover_image.
Instead of {{author.cover}} you need to use {{author.cover_image}}.
See the object attributes of author here.`, regex: /{{\s*?author\.cover\s*?}}/g, helper: '{{author.cover}}' }, 'GS001-DEPR-TIMG': { level: 'error', rule: 'Replace {{tag.image}} with {{tag.feature_image}}', fatal: true, details: oneLineTrim`The image attribute in tag context was replaced with feature_image.
Instead of {{tag.image}} you need to use {{tag.feature_image}}.
See the object attributes of tags here.`, regex: /{{\s*?tag\.image\s*?}}/g, helper: '{{tag.image}}' }, 'GS001-DEPR-PAIMG': { level: 'error', rule: 'Replace {{post.author.image}} with {{post.author.feature_image}}', fatal: true, details: oneLineTrim`The image attribute in author context was replaced with feature_image.
Instead of {{post.author.image}} you need to use {{post.author.feature_image}}.
See the object attributes of author here.`, regex: /{{\s*?post\.author\.image\s*?}}/g, helper: '{{post.author.image}}' }, 'GS001-DEPR-PAC': { level: 'error', rule: 'Replace {{post.author.cover}} with {{post.author.cover_image}}', fatal: true, details: oneLineTrim`The cover attribute in author context was replaced with cover_image.
Instead of {{post.author.cover}} you need to use {{post.author.cover_image}}.
See the object attributes of author here.`, regex: /{{\s*?post\.author\.cover\s*?}}/g, helper: '{{post.author.cover}}' }, 'GS001-DEPR-PTIMG': { level: 'error', rule: 'Replace {{post.tags.[#].image}} with {{post.tags.[#].feature_image}}', fatal: true, details: oneLineTrim`The image attribute in tag context was replaced with feature_image.
Instead of {{post.tags.[#].image}} you need to use {{post.tags.[#].feature_image}}.
See the object attributes of tags here.`, regex: /{{\s*?post\.tags\.\[[0-9]+\]\.image\s*?}}/g, helper: '{{post.tags.[#].image}}' }, 'GS001-DEPR-TSIMG': { level: 'error', rule: 'Replace {{tags.[#].image}} with {{tags.[#].feature_image}}', fatal: true, details: oneLineTrim`The image attribute in tag context was replaced with feature_image.
Instead of {{tags.[#].image}} you need to use {{tags.[#].feature_image}}.
See the object attributes of tags here.`, regex: /{{\s*?tags\.\[[0-9]+\]\.image\s*?}}/g, helper: '{{tags.[#].image}}' }, 'GS001-DEPR-CON-IMG': { level: 'error', rule: 'Replace {{#if image}} with {{#if feature_image}}, or ' + '{{#if profile_image}}', fatal: true, details: oneLineTrim`The image attribute was replaced with feature_image and profile_image.
Depending on the context you will need to replace it like this:

{{#author}}
    {{#if profile_image}}
        {{profile_image}}
    {{/if}}
{{/author}}


See the object attributes of author here.

{{#post}}
    {{#if feature_image}}
        {{feature_image}}
    {{/if}}
{{/post}}


See the object attributes of post here.

{{#tag}}
    {{#if feature_image}}
        {{feature_image}}
    {{/if}}
{{/tag}}


See the object attributes of tags here.`, regex: /{{\s*?#if\s*?image\s*?}}/g, helper: '{{#if image}}' }, 'GS001-DEPR-CON-COV': { level: 'error', rule: 'Replace {{#if cover}} with {{#if cover_image}}', fatal: true, details: oneLineTrim`The cover attribute was replaced with cover_image. To check for the cover image in author context, you need to use

{{#if cover_image}}
    {{cover_image}}
{{/if}}


See the object attributes of author here.
To check for the cover image of your blog, just use {{#if @site.cover_image}}. See here.`, regex: /{{\s*?#if\s*?cover\s*?}}/g, helper: '{{#if cover}}' }, 'GS001-DEPR-CON-BC': { level: 'error', rule: 'Replace {{#if @blog.cover}} with {{#if @site.cover_image}}', fatal: true, details: oneLineTrim`The cover attribute was replaced with cover_image.
Instead of {{#if @blog.cover}} you need to use {{#if @site.cover_image}}.
See here.`, regex: /{{\s*?#if\s*?@blog\.cover\s*?}}/g, helper: '{{#if @blog.cover}}' }, 'GS001-DEPR-CON-AC': { level: 'error', rule: 'Replace {{#if author.cover}} with {{#if author.cover_image}}', fatal: true, details: oneLineTrim`The cover attribute was replaced with cover_image.
Instead of {{#if author.cover}} you need to use {{#if author.cover_image}}.
See the object attributes of author here.`, regex: /{{\s*?#if\s*?author\.cover\s*?}}/g, helper: '{{#if author.cover}}' }, 'GS001-DEPR-CON-AIMG': { level: 'error', rule: 'Replace {{#if author.image}} with {{#if author.profile_image}}', fatal: true, details: oneLineTrim`The image attribute in author context was replaced with profile_image.
Instead of {{#if author.image}} you need to use {{#if author.profile_image}}.
See the object attributes of author here.`, regex: /{{\s*?#if\s*?author\.image\s*?}}/g, helper: '{{#if author.image}}' }, 'GS001-DEPR-CON-PAC': { level: 'error', rule: 'Replace {{#if post.author.cover}} with {{#if post.author.cover_image}}', fatal: true, details: oneLineTrim`The cover attribute was replaced with cover_image.
Instead of {{#if post.author.cover}} you need to use {{#if post.author.cover_image}}.
See the object attributes of author here.`, regex: /{{\s*?#if\s*?post\.author\.cover\s*?}}/g, helper: '{{#if post.author.cover}}' }, 'GS001-DEPR-CON-PAIMG': { level: 'error', rule: 'Replace {{#if post.author.image}} with {{#if post.author.profile_image}}', fatal: true, details: oneLineTrim`The image attribute in author context was replaced with profile_image.
Instead of {{#if post.author.image}} you need to use {{#if post.author.profile_image}}.
See the object attributes of author here.`, regex: /{{\s*?#if\s*?post\.author\.image\s*?}}/g, helper: '{{#if post.author.image}}' }, 'GS001-DEPR-CON-TIMG': { level: 'error', rule: 'Replace {{#if tag.image}} with {{#if tag.feature_image}}', fatal: true, details: oneLineTrim`The image attribute in tag context was replaced with feature_image.
Instead of {{#if tag.image}} you need to use {{#if tag.feature_image}}.
See the object attributes of tags here.`, regex: /{{\s*?#if\s*?tag\.image\s*?}}/g, helper: '{{#if tag.image}}' }, 'GS001-DEPR-CON-PTIMG': { level: 'error', rule: 'Replace {{#if post.tags.[#].image}} with {{#if post.tags.[#].feature_image}}', fatal: true, details: oneLineTrim`The image attribute in tag context was replaced with feature_image.
Instead of {{#if post.tags.[#].image}} you need to use {{#if post.tags.[#].feature_image}}.
See the object attributes of tags here.`, regex: /{{\s*?#if\s*?post\.tags\.\[[0-9]+\].image\s*?}}/g, helper: '{{#if posts.tags.[#].image}}' }, 'GS001-DEPR-CON-TSIMG': { level: 'error', rule: 'Replace {{#if tags.[#].image}} with {{#if tags.[#].feature_image}}', fatal: true, details: oneLineTrim`The image attribute in tag context was replaced with feature_image.
Instead of {{#if tags.[#].image}} you need to use {{#if tags.[#].feature_image}}.
See the object attributes of tags here.`, regex: /{{\s*?#if\s*?tags\.\[[0-9]+\].image\s*?}}/g, helper: '{{#if tags.[#].image}}' }, 'GS001-DEPR-PPP': { level: 'error', rule: 'Replace {{@blog.posts_per_page}} with {{@config.posts_per_page}}', details: oneLineTrim`The global {{@blog.posts_per_page}} property was replaced with {{@config.posts_per_page}}.
Read here about the attribute and check here where you can customise the posts per page setting, as this is now adjustable in your theme.`, regex: /{{\s*?@blog\.posts_per_page\s*?}}/g, helper: '{{@blog.posts_per_page}}' }, 'GS001-DEPR-C0H': { level: 'error', rule: 'Replace {{content words="0"}} with <img src="{{img_url feature_image}}"/>.', details: oneLineTrim`The {{content words="0"}} hack doesn't work anymore (and was never supported).
Find more information about the {{img_url}} helper here.`, regex: /{{\s*?content words=("|')0("|')\s*?}}/g, helper: '{{content words="0"}}' }, 'GS001-DEPR-CSS-AT': { level: 'error', rule: 'Replace .archive-template with the .paged CSS class', details: oneLineTrim`The .archive-template CSS class was replaced with the .paged. Please replace this in your stylesheet.
See the context table to check which classes Ghost uses for each context.`, regex: /\.archive-template[\s{]/g, className: '.archive-template', css: true }, 'GS001-DEPR-CSS-PATS': { level: 'error', rule: 'Replace .page-template-slug with the .page-slug css class', details: oneLineTrim`The .page-template-slug CSS class was replaced with the .page-slug. Please replace this in your stylesheet.
See the context table to check which classes Ghost uses for each context.`, regex: /\.page-template-\w+[\s{]/g, className: '.page-template-slug', css: true }, 'GS001-DEPR-EACH': { level: 'warning', rule: 'Replace {{#each}} with {{#foreach}}', fatal: false, details: oneLineTrim`The {{#foreach}} helper is context-aware and should always be used instead of Handlebars {{#each}} when working with Ghost themes.
See the description of {{#foreach}} helper here.`, regex: /{{\s*?#each\s*/g }, 'GS002-DISQUS-ID': { level: 'error', rule: 'Replace {{id}} with {{comment_id}} in Disqus embeds.', fatal: true, details: oneLineTrim`The output of {{id}} has changed in v1.0.0 from an incremental ID to an ObjectID. This results in Disqus comments not loading in Ghost v1.0.0 posts which were imported from earlier versions. To resolve this, we've added a {{comment_id}} helper that will output the old ID for posts that have been imported from earlier versions, and ID for new posts. The Disqus embed must be updated from this.page.identifier = 'ghost-{{id}}'; to this.page.identifier = 'ghost-{{comment_id}}'; to ensure Disqus continues to work.`, regex: /(page\.|disqus_)identifier\s?=\s?['"].*?({{\s*?id\s*?}}).*?['"];?/g }, 'GS002-ID-HELPER': { level: 'recommendation', rule: 'The output of {{id}} changed in Ghost v1.0.0, you may need to use {{comment_id}} instead.', details: oneLineTrim`The output of {{id}} has changed in v1.0.0 from an incremental ID to an ObjectID. In v1.0.0 we added a {{comment_id}} helper that will output the old ID for posts that have been imported from earlier versions, and ID for new posts. If you need the old ID to be output on imported posts, then you will need to use {{comment_id}} rather than {{id}}.`, regex: /({{\s*?id\s*?}})/g }, 'GS005-TPL-ERR': { level: 'error', rule: 'Templates must contain valid Handlebars', fatal: true, details: oneLineTrim`Oops! You seemed to have used invalid Handlebars syntax. This mostly happens when you use a helper that is not supported.
See the full list of available helpers here.` }, 'GS010-PJ-REQ': { level: 'error', rule: 'package.json file should be present', details: oneLineTrim`You should provide a package.json file for your theme.
Check the package.json documentation to see which properties are required and which are recommended.` }, 'GS010-PJ-PARSE': { level: 'error', rule: 'package.json file can be parsed', details: oneLineTrim`Your package.json file couldn't be parsed. This is mostly caused by a missing or unnecessary ',' or the wrong usage of '""'.
Check the package.json documentation for further information.
A good reference for your package.json file is always the latest version of Casper.` }, 'GS010-PJ-NAME-LC': { level: 'error', rule: 'package.json property "name" must be lowercase', details: oneLineTrim`The property "name" in your package.json file must be lowercase.
Good examples are: "my-theme" or "theme" rather than "My Theme" or "Theme".
Check the package.json documentation for further information.` }, 'GS010-PJ-NAME-HY': { level: 'error', rule: 'package.json property "name" must be hyphenated', details: oneLineTrim`The property "name" in your package.json file must be hyphenated.
Please use "my-theme" rather than "My Theme" or "my theme".
Check the package.json documentation for further information.` }, 'GS010-PJ-NAME-REQ': { level: 'error', rule: 'package.json property "name" is required', details: oneLineTrim`Please add the property "name" to your package.json. E.g. {"name": "my-theme"}.
Check the package.json documentation to see which properties are required and which are recommended.` }, 'GS010-PJ-VERSION-SEM': { level: 'error', rule: 'package.json property "version" must be semver compliant', details: oneLineTrim`The property "version" in your package.json file must be semver compliant. E.g. {"version": "1.0.0"}.
Check the package.json documentation for further information.` }, 'GS010-PJ-VERSION-REQ': { level: 'error', rule: 'package.json property "version" is required', details: oneLineTrim`Please add the property "version" to your package.json. E.g. {"version": "1.0.0"}.
Check the package.json documentation to see which properties are required and which are recommended.` }, 'GS010-PJ-AUT-EM-VAL': { level: 'error', rule: 'package.json property "author.email" must be valid', details: oneLineTrim`The property "author.email" in your package.json file must a valid email. E.g. {"author": {"email": "hello@example.com"}}.
Check the package.json documentation for further information.` }, 'GS010-PJ-CONF-PPP': { level: 'recommendation', rule: 'package.json property "config.posts_per_page" is recommended. Otherwise, it falls back to 5', details: oneLineTrim`Please add "posts_per_page" to your package.json. E.g. {"config": { "posts_per_page": 5}}.
If no "posts_per_page" property is provided, Ghost will use its default setting of 5 posts per page.
Check the package.json documentation for further information.` }, 'GS010-PJ-CONF-PPP-INT': { level: 'error', rule: 'package.json property "config.posts_per_page" must be a number above 0', details: oneLineTrim`The property "config.posts_per_page" in your package.json file must be a number greater than zero. E.g. {"config": { "posts_per_page": 5}}.
Check the package.json documentation for further information.` }, 'GS010-PJ-AUT-EM-REQ': { level: 'error', rule: 'package.json property "author.email" is required', details: oneLineTrim`Please add the property "author.email" to your package.json. E.g. {"author": {"email": "hello@example.com"}}.
The email is required so that themes which are distributed (either free or paid) have a method of contacting the author so users can get support and more importantly so that> Ghost can reach out about breaking changes and security updates.
The package.json file is NOT accessible when uploaded to a blog so if the theme is only uploaded to a single blog, no one will see this email address.
Check the package.json documentation to see which properties are required and which are recommended.` }, 'GS020-INDEX-REQ': { level: 'error', rule: 'A template file called index.hbs must be present', fatal: true, details: oneLineTrim`Your theme must have a template file called index.hbs.
Read here more about the required template structure and index.hbs in particular.`, path: 'index.hbs' }, 'GS020-POST-REQ': { level: 'error', rule: 'A template file called post.hbs must be present', fatal: true, details: oneLineTrim`Your theme must have a template file called index.hbs.
Read here more about the required template structure and post.hbs in particular.`, path: 'post.hbs' }, 'GS020-DEF-REC': { level: 'recommendation', rule: 'Provide a default layout template called default.hbs', details: oneLineTrim`It is recommended that your theme has a template file called default.hbs.
Read here more about the recommended template structure and default.hbs in particular.`, path: 'default.hbs' }, 'GS030-ASSET-REQ': { level: 'warning', rule: 'Assets such as CSS & JS must use the {{asset}} helper', details: oneLineTrim`The listed files should be included using the {{asset}} helper.
For more information, please see the {{asset}} helper documentation.`, regex: /(src|href)=['"](.*?\/assets\/.*?)['"]/gmi }, 'GS030-ASSET-SYM': { level: 'error', rule: 'Symlinks in themes are not allowed', fatal: true, details: oneLineTrim`Symbolic links in themes are not allowed. Please use the {{asset}} helper.
For more information, please see the {{asset}} helper documentation.` }, 'GS040-GH-REQ': { level: 'warning', rule: 'The helper {{ghost_head}} should be present', details: oneLineTrim`The {{ghost_head}} helper should be present in your theme. It outputs many useful things, such as "code injection" scripts, structured data, canonical links, meta description etc.
The helper belongs just before the </head> tag in your default.hbs template.
For more details, please see the {{ghost_head}} helper documentation.`, helper: 'ghost_head' }, 'GS040-GF-REQ': { level: 'warning', rule: 'The helper {{ghost_foot}} should be present', details: oneLineTrim`The {{ghost_foot}} helper should be present in your theme. It outputs scripts as saved in "code injection" scripts.
The helper belongs just before the </body> tag in your default.hbs template.
For more details, please see the {{ghost_foot}} helper documentation.`, helper: 'ghost_foot' } }; /** * These are rules that haven't been implemented yet, but should be! */ ruleNext = { //eslint-disable-line 'GS030-CSS-CACHE': { level: 'warning', rule: 'CSS files should use cache bustable URLs' } }; module.exports = { knownHelpers: knownHelpers, templates: templates, rules: rules };