/**
* # 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
};