Added Source as the new default theme

refs TryGhost/Product#3510

- Added `TryGhost/Source` as a submodule in `ghost/core/content/themes` so `Source` will ship with Ghost (along with Casper)
- With this change, new installs will use `Source` as the default theme. Existing sites will have `Source` installed, but not activated, as this is a large change and we don't want to drastically change existing sites without warning. Users can upgrade to use `Source` simply by clicking 'Activate' in design settings.
- Updated protections to prevent users from uploading their own conflicting version of `Source`
This commit is contained in:
Chris Raible 2023-09-06 13:22:12 -07:00 committed by Daniel Lockyer
parent add7f1283b
commit 80a6fe17d0
82 changed files with 1294 additions and 124 deletions

1
.gitignore vendored
View File

@ -97,6 +97,7 @@ typings/
/ghost/core/content/adapters/storage/**/*
/ghost/core/content/adapters/scheduling/**/*
/ghost/core/content/themes/casper
/ghost/core/content/themes/source
!/ghost/core/README.md
!/ghost/core/content/**/README.md

4
.gitmodules vendored
View File

@ -2,3 +2,7 @@
path = ghost/core/content/themes/casper
url = ../../TryGhost/Casper.git
ignore = all
[submodule "ghost/core/content/themes/source"]
path = ghost/core/content/themes/source
url = ../../TryGhost/Source.git
ignore = all

View File

@ -133,9 +133,13 @@ export function isActiveTheme(theme: Theme): boolean {
}
export function isDefaultTheme(theme: Theme): boolean {
return theme.name === 'source';
}
export function isLegacyTheme(theme: Theme): boolean {
return theme.name === 'casper';
}
export function isDeletableTheme(theme: Theme): boolean {
return !isDefaultTheme(theme) && !isActiveTheme(theme);
return !isDefaultTheme(theme) && !isLegacyTheme(theme) && !isActiveTheme(theme);
}

View File

@ -7,7 +7,7 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
import NiceModal from '@ebay/nice-modal-react';
import React from 'react';
import useHandleError from '../../../../utils/api/handleError';
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, isLegacyTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
interface ThemeActionProps {
@ -23,6 +23,8 @@ function getThemeLabel(theme: Theme): React.ReactNode {
if (isDefaultTheme(theme)) {
label += ' (default)';
} else if (isLegacyTheme(theme)) {
label += ' (legacy)';
} else if (theme.package?.name !== theme.name) {
label =
<span className='text-sm md:text-base'>

View File

@ -4,6 +4,13 @@ import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
import React from 'react';
import {OfficialTheme, useOfficialThemes} from '../../../providers/ServiceProvider';
import {getGhostPaths, resolveAsset} from '../../../../utils/helpers';
import {useEffect, useState} from 'react';
const sourceDemos = [
{image: 'Source.png', category: 'News'},
{image: 'Source-Magazine.png', category: 'Magazine'},
{image: 'Source-Newsletter.png', category: 'Newsletter'}
];
const OfficialThemes: React.FC<{
onSelectTheme?: (theme: OfficialTheme) => void;
@ -12,6 +19,20 @@ const OfficialThemes: React.FC<{
}) => {
const {adminRoot} = getGhostPaths();
const officialThemes = useOfficialThemes();
const [currentSourceDemoIndex, setCurrentSourceDemoIndex] = useState(0);
const [isHovered, setIsHovered] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
if (isHovered) {
setCurrentSourceDemoIndex(prevIndex => (prevIndex + 1) % sourceDemos.length);
}
}, 3000);
return () => {
clearInterval(interval);
};
}, [isHovered]);
return (
<ModalPage heading='Themes'>
@ -22,16 +43,33 @@ const OfficialThemes: React.FC<{
onSelectTheme?.(theme);
}}>
{/* <img alt={theme.name} src={`${assetRoot}/${theme.image}`}/> */}
<div className='w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]'>
<div className='relative w-full bg-grey-100 shadow-md transition-all duration-500 hover:scale-[1.05]' onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
{theme.name !== 'Source' ?
<img
alt={`${theme.name} Theme`}
className='h-full w-full object-contain'
src={resolveAsset(theme.image, adminRoot)}
/> :
<>
{sourceDemos.map((demo, index) => (
<img
key={`source-theme-${demo.category}`}
alt={`${theme.name} Theme - ${demo.category}`}
className={`${index === 0 ? 'relative' : 'absolute'} left-0 top-0 h-full w-full object-contain transition-opacity duration-500 ${index === currentSourceDemoIndex ? 'opacity-100' : 'opacity-0'}`}
src={resolveAsset(`assets/img/themes/${demo.image}`, adminRoot)}
/>
))}
</>
}
</div>
<div className='mt-3'>
<div className='relative mt-3'>
<Heading level={4}>{theme.name}</Heading>
<span className='text-sm text-grey-700'>{theme.category}</span>
{theme.name !== 'Source' ?
<span className='text-sm text-grey-700'>{theme.category}</span> :
sourceDemos.map((demo, index) => (
<span className={`${index === 0 ? 'relative' : 'absolute bottom-[1px]'} left-0 inline-block w-24 bg-white text-sm text-grey-700 ${index === currentSourceDemoIndex ? 'opacity-100' : 'opacity-0'}`}>{demo.category}</span>
))
}
</div>
</button>
);

View File

@ -7,9 +7,16 @@ import MobileChrome from '../../../../admin-x-ds/global/chrome/MobileChrome';
import NiceModal from '@ebay/nice-modal-react';
import PageHeader from '../../../../admin-x-ds/global/layout/PageHeader';
import React, {useState} from 'react';
import Select, {SelectOption} from '../../../../admin-x-ds/global/form/Select';
import {OfficialTheme} from '../../../providers/ServiceProvider';
import {Theme} from '../../../../api/themes';
const sourceDemos = [
{label: 'News', value: 'news', url: 'https://source.ghost.io'},
{label: 'Magazine', value: 'magazine', url: 'https://source-magazine.ghost.io'},
{label: 'Newsletter', value: 'newsletter', url: 'https://source-newsletter.ghost.io'}
];
const ThemePreview: React.FC<{
selectedTheme?: OfficialTheme;
isInstalling?: boolean;
@ -26,6 +33,7 @@ const ThemePreview: React.FC<{
onInstall
}) => {
const [previewMode, setPreviewMode] = useState('desktop');
const [currentSourceDemo, setCurrentSourceDemo] = useState<SelectOption>(sourceDemos[0]);
if (!selectedTheme) {
return null;
@ -68,6 +76,7 @@ const ThemePreview: React.FC<{
<div className='flex items-center gap-2'>
<Breadcrumbs
activeItemClassName='hidden md:!block md:!visible'
containerClassName='whitespace-nowrap'
itemClassName='hidden md:!block md:!visible'
items={[
{label: 'Design', onClick: onClose},
@ -78,6 +87,24 @@ const ThemePreview: React.FC<{
backIcon
onBack={onBack}
/>
{selectedTheme.name === 'Source' ?
<>
<span className='hidden md:!visible md:!block'></span>
<Select
border={false}
containerClassName='text-sm font-bold'
controlClasses={{menu: 'w-24'}}
fullWidth={false}
options={sourceDemos}
selectedOption={currentSourceDemo}
onSelect={(option) => {
if (option) {
setCurrentSourceDemo(option);
}
}}
/>
</> : null
}
</div>;
const right =
@ -118,13 +145,19 @@ const ThemePreview: React.FC<{
<div className='flex h-[calc(100%-74px)] grow flex-col items-center justify-center bg-grey-50 dark:bg-black'>
{previewMode === 'desktop' ?
<DesktopChrome>
<iframe className='h-full w-full'
src={selectedTheme?.previewUrl} title='Theme preview' />
<iframe
className='h-full w-full'
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
title='Theme preview'
/>
</DesktopChrome>
:
<MobileChrome>
<iframe className='h-full w-full'
src={selectedTheme?.previewUrl} title='Theme preview' />
<iframe
className='h-full w-full'
src={selectedTheme.name !== 'Source' ? selectedTheme?.previewUrl : sourceDemos.find(demo => demo.label === currentSourceDemo.label)?.url}
title='Theme preview'
/>
</MobileChrome>
}
</div>

View File

@ -13,6 +13,12 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
}}
ghostVersion='5.x'
officialThemes={[{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png'
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',

View File

@ -12,18 +12,17 @@ import {tracked} from '@glimmer/tracking';
// TODO: Long term move asset management directly in AdminX
const officialThemes = [{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png'
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',
ref: 'default',
ref: 'TryGhost/Casper',
image: 'assets/img/themes/Casper.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Edition',
category: 'Newsletter',
@ -108,6 +107,13 @@ const officialThemes = [{
previewUrl: 'https://ease.ghost.io',
ref: 'TryGhost/Ease',
image: 'assets/img/themes/Ease.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Ruby',
category: 'Magazine',

View File

@ -18,6 +18,14 @@ export default class GhThemeTableComponent extends Component {
this.activateTaskInstance?.cancel();
}
isDefaultTheme(theme) {
return theme.name.toLowerCase() === 'source';
}
isLegacyTheme(theme) {
return theme.name.toLowerCase() === 'casper';
}
get sortedThemes() {
let themes = this.args.themes.map((t) => {
let theme = {};
@ -30,7 +38,6 @@ export default class GhThemeTableComponent extends Component {
theme.package = themePackage;
theme.active = get(t, 'active');
theme.isDeletable = !theme.active;
return theme;
});
let duplicateThemes = [];
@ -44,19 +51,24 @@ export default class GhThemeTableComponent extends Component {
});
duplicateThemes.forEach((theme) => {
if (theme.name !== 'casper') {
if (!this.isDefaultTheme(theme) && !this.isLegacyTheme(theme)) {
theme.label = `${theme.label} (${theme.name})`;
}
});
// "(default)" needs to be added to casper manually as it's always
// displayed and would mess up the duplicate checking if added earlier
let casper = themes.findBy('name', 'casper');
if (casper) {
casper.label = `${casper.label} (default)`;
casper.isDefault = true;
casper.isDeletable = false;
}
// add (default) or (legacy) as appropriate and prevent deletion of default/legacy themes
// this needs to be after deduplicating by label
themes.filter(this.isDefaultTheme).forEach((theme) => {
theme.label = `${theme.label} (default)`;
theme.isDefault = true;
theme.isDeletable = false;
});
themes.filter(this.isLegacyTheme).forEach((theme) => {
theme.label = `${theme.label} (legacy)`;
theme.isLegacy = true;
theme.isDeletable = false;
});
// sorting manually because .sortBy('label') has a different sorting
// algorithm to [...strings].sort()

View File

@ -31,8 +31,8 @@ export default class InstallThemeModal extends Component {
return this.args.data.theme?.ref || this.args.data.ref;
}
get isDefaultTheme() {
return this.themeName.toLowerCase() === 'casper';
get isDefaultOrLegacyTheme() {
return this.themeName.toLowerCase() === 'casper' || this.themeName.toLowerCase() === 'source';
}
get isConfirming() {
@ -48,7 +48,7 @@ export default class InstallThemeModal extends Component {
}
get willOverwriteExisting() {
return !this.isDefaultTheme && this.themes.findBy('name', this.themeName.toLowerCase());
return !this.isDefaultOrLegacyTheme && this.themes.findBy('name', this.themeName.toLowerCase());
}
get hasWarningsOrErrors() {
@ -67,9 +67,10 @@ export default class InstallThemeModal extends Component {
@task
*installThemeTask() {
try {
if (this.isDefaultTheme) {
if (this.isDefaultOrLegacyTheme) {
// default theme can't be installed, only activated
const defaultTheme = this.store.peekRecord('theme', 'casper');
const themeName = this.themeName.toLowerCase();
const defaultTheme = this.store.peekRecord('theme', themeName);
yield this.themeManagement.activateTask.perform(defaultTheme, {skipErrors: true});
this.installedTheme = defaultTheme;

View File

@ -91,8 +91,8 @@ export default class UploadThemeModal extends Component {
return new UnsupportedMediaTypeError();
}
if (file.name.match(/^casper\.zip$/i)) {
return {payload: {errors: [{message: 'Sorry, the default Casper theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
if (file.name.match(/^casper\.zip$/i) || file.name.match(/^source\.zip$/i)) {
return {payload: {errors: [{message: 'Sorry, the default theme cannot be overwritten.<br>Please rename your zip file to continue.'}]}};
}
if (!this._allowOverwrite && this.currentThemeNames.includes(themeName)) {

View File

@ -13,18 +13,17 @@ export default class ChangeThemeController extends Controller {
themes = this.store.peekAll('theme');
officialThemes = [{
name: 'Source',
category: 'News',
previewUrl: 'https://source.ghost.io/',
ref: 'default',
image: 'assets/img/themes/Source.png'
}, {
name: 'Casper',
category: 'Blog',
previewUrl: 'https://demo.ghost.io/',
ref: 'default',
ref: 'TryGhost/Casper',
image: 'assets/img/themes/Casper.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Edition',
category: 'Newsletter',
@ -109,6 +108,13 @@ export default class ChangeThemeController extends Controller {
previewUrl: 'https://ease.ghost.io',
ref: 'TryGhost/Ease',
image: 'assets/img/themes/Ease.png'
}, {
name: 'Headline',
category: 'News',
url: 'https://github.com/TryGhost/Headline',
previewUrl: 'https://headline.ghost.io',
ref: 'TryGhost/Headline',
image: 'assets/img/themes/Headline.png'
}, {
name: 'Ruby',
category: 'Magazine',

View File

@ -6,6 +6,7 @@ import {tracked} from '@glimmer/tracking';
const THEME_PROPERTIES = {
casper: ['description', 'color', 'coverImage'],
source: ['description', 'color', 'coverImage'],
edition: ['description', 'color', 'coverImage'],
dawn: ['description', 'color', 'icon'],
dope: ['description', 'color', 'logo'],

View File

@ -19,7 +19,7 @@ function setting(group, key, value) {
}
// These settings represent a default new site setup
// Real default settings can be found in https://github.com/TryGhost/Ghost/blob/main/core/server/data/schema/default-settings/default-settings.json
// Real default settings can be found in https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/data/schema/default-settings/default-settings.json
export default [
// SITE
setting('site', 'title', 'Test Blog'),
@ -49,7 +49,7 @@ export default [
setting('site', 'twitter_description', null),
// THEME
setting('theme', 'active_theme', 'Casper'),
setting('theme', 'active_theme', 'Source'),
// PRIVATE
setting('private', 'is_private', false),

View File

@ -1,11 +1,18 @@
export default [
{
name: 'source',
package: {
name: 'source',
version: '1.0'
},
active: true
},
{
name: 'casper',
package: {
name: 'casper',
version: '1.0'
},
active: true
}
},
{
name: 'foo',

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -46,7 +46,7 @@ describe('Acceptance: Settings - Design', function () {
expect(findAll('[data-test-nav-group]'), 'no of groups open').to.have.lengthOf(1);
// current theme is shown in nav menu
expect(find('[data-test-text="current-theme"]')).to.contain.text('casper - v1.0');
expect(find('[data-test-text="current-theme"]')).to.contain.text('source - v1.0');
// defaults to "home" desktop preview
expect(find('[data-test-button="desktop-preview"]')).to.have.class('gh-btn-group-selected');
@ -143,7 +143,7 @@ describe('Acceptance: Settings - Design', function () {
config.hostSettings = {
limits: {
customThemes: {
allowlist: ['casper', 'dawn', 'lyra'],
allowlist: ['source', 'casper', 'dawn', 'lyra'],
error: 'All our official built-in themes are available the Starter plan, if you upgrade to one of our higher tiers you will also be able to edit and upload custom themes for your site.'
}
}

View File

@ -11,6 +11,7 @@ describe('Integration: Component: gh-theme-table', function () {
this.set('themes', [
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
{name: 'source', package: {name: 'Source', version: '1.0.0'}},
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
{name: 'foo'}
]);
@ -18,14 +19,15 @@ describe('Integration: Component: gh-theme-table', function () {
await render(hbs`<GhThemeTable @themes={{themes}} />`);
expect(findAll('[data-test-themes-list]').length, 'themes list is present').to.equal(1);
expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(4);
expect(findAll('[data-test-theme-id]').length, 'number of rows').to.equal(5);
let packageNames = findAll('[data-test-theme-title]').map(name => name.textContent.trim());
expect(packageNames[0]).to.match(/Casper \(default\)/);
expect(packageNames[0]).to.match(/Casper \(legacy\)/);
expect(packageNames[1]).to.match(/Daring\s+Active/);
expect(packageNames[2]).to.match(/foo/);
expect(packageNames[3]).to.match(/Lanyon/);
expect(packageNames[4]).to.match(/Source \(default\)/);
expect(
find('[data-test-theme-active="true"]').querySelector('[data-test-theme-title]'),
@ -35,7 +37,7 @@ describe('Integration: Component: gh-theme-table', function () {
expect(
findAll('[data-test-button="activate"]').length,
'non-active themes have an activate link'
).to.equal(3);
).to.equal(4);
expect(
find('[data-test-theme-active="true"]').querySelector('[data-test-button="activate"]'),
@ -80,22 +82,31 @@ describe('Integration: Component: gh-theme-table', function () {
}
});
it('does not show delete action for casper', async function () {
it('does not show delete action for default themes', async function () {
const themes = [
{name: 'Daring', package: {name: 'Daring', version: '0.1.4'}, active: true},
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
{name: 'oscar-ghost-1.1.0', package: {name: 'Lanyon', version: '1.1.0'}},
{name: 'foo'}
{name: 'foo'},
{name: 'source', package: {name: 'Source', version: '1.0.0'}}
];
this.set('themes', themes);
await render(hbs`<GhThemeTable @themes={{themes}} />`);
// Casper should not be deletable
await click(`[data-test-theme-id="casper"] [data-test-button="actions"]`);
expect(find('[data-test-actions-for="casper"]')).to.exist;
expect(
find(`[data-test-actions-for="casper"] [data-test-button="delete"]`)
).to.not.exist;
// Source should not be deletable
await click(`[data-test-theme-id="source"] [data-test-button="actions"]`);
expect(find('[data-test-actions-for="source"]')).to.exist;
expect(
find(`[data-test-actions-for="source"] [data-test-button="delete"]`)
).to.not.exist;
});
it('does not show delete action for active theme', async function () {
@ -120,6 +131,7 @@ describe('Integration: Component: gh-theme-table', function () {
this.set('themes', [
{name: 'daring', package: {name: 'Daring', version: '0.1.4'}},
{name: 'daring-0.1.5', package: {name: 'Daring', version: '0.1.4'}},
{name: 'source', package: {name: 'Source', version: '1.0.0'}},
{name: 'casper', package: {name: 'Casper', version: '1.3.1'}},
{name: 'another', package: {name: 'Casper', version: '1.3.1'}},
{name: 'mine', package: {name: 'Casper', version: '1.3.1'}},
@ -135,11 +147,12 @@ describe('Integration: Component: gh-theme-table', function () {
'themes are ordered by label, folder names shown for duplicates'
).to.deep.equal([
'Casper (another)',
'Casper (default)',
'Casper (legacy)',
'Casper (mine)',
'Daring (daring)',
'Daring (daring-0.1.5)',
'foo'
'foo',
'Source (default)'
]);
});
});

View File

@ -32,6 +32,8 @@ content/settings/**
content/themes/**
!content/themes/casper/**
content/themes/casper/yarn.lock
!content/themes/source/**
content/themes/source/yarn.lock
node_modules/**
core/server/lib/members/static/auth/node_modules/**
**/*.db
@ -50,6 +52,7 @@ core/built/**/tests-*
test/**
CONTRIBUTING.md
content/themes/casper/SECURITY.md
content/themes/source/SECURITY.md
SECURITY.md
renovate.json
*.html
@ -61,7 +64,9 @@ bower_components/**
.editorconfig
gulpfile.js
!content/themes/casper/gulpfile.js
!content/themes/source/gulpfile.js
package-lock.json
content/themes/casper/config.*.json
content/themes/source/config.*.json
config.*.json
!core/shared/config/env/**

@ -0,0 +1 @@
Subproject commit e0483fd7f4d6a9e2d5c26c1e89d4322f1f2702d6

View File

@ -0,0 +1,40 @@
// For information on writing migrations, see https://www.notion.so/ghost/Database-migrations-eb5b78c435d741d2b34a582d57c24253
const logging = require('@tryghost/logging');
const {createTransactionalMigration} = require('../../utils');
// For DDL - schema changes
// const {createNonTransactionalMigration} = require('../../utils');
// For DML - data changes
// const {createTransactionalMigration} = require('../../utils');
// Or use a specific helper
// const {addTable, createAddColumnMigration} = require('../../utils');
module.exports = createTransactionalMigration(
async function up() {
// don't do anything
// we don't want to change the active theme automatically
},
async function down(knex) {
// If the active theme is `source`, we want to revert the active theme to `casper`
// `source` is introduced in 5.67, so it does not exist in < 5.67
// Without this change, rolling back from 5.67 to < 5.67 results in an error:
// The currently active theme "source" is missing.
const rows = await knex
.select('value')
.from('settings')
.where({key: 'active_theme', value: 'source'});
if (rows.length === 0) {
logging.info(`Currently installed theme is not source - skipping migration`);
return;
}
logging.info(`Resetting the active theme to casper`);
await knex('settings')
.where('key', 'active_theme')
.update({value: 'casper'});
}
);

View File

@ -216,7 +216,7 @@
},
"theme": {
"active_theme": {
"defaultValue": "casper",
"defaultValue": "source",
"flags": "RO",
"type": "string"
}

View File

@ -200,8 +200,8 @@ async function installTheme(data, api) {
return data;
}
if (themeName.toLowerCase() === 'tryghost/casper') {
logging.warn('Skipping theme install as Casper is the default theme.');
if (themeName.toLowerCase() === 'tryghost/source') {
logging.warn('Skipping theme install as Source is the default theme.');
return data;
}
@ -219,7 +219,7 @@ async function installTheme(data, api) {
context: {internal: true}
});
} catch (error) {
//Fallback to Casper by doing nothing as the theme setting update is the last step
//Fallback to Casper/Source by doing nothing as the theme setting update is the last step
logging.warn(tpl(messages.failedThemeInstall, {themeName, error: error.message}));
}

View File

@ -18,8 +18,8 @@ const settingsCache = require('../../../shared/settings-cache');
const messages = {
themeDoesNotExist: 'Theme does not exist.',
invalidThemeName: 'Please select a valid theme.',
overrideCasper: 'Please rename your zip, it\'s not allowed to override the default casper theme.',
destroyCasper: 'Deleting the default casper theme is not allowed.',
overrideDefaultTheme: 'Please rename your zip, it\'s not allowed to override the default theme.',
destroyDefaultTheme: 'Deleting the default theme is not allowed.',
destroyActive: 'Deleting the active theme is not allowed.'
};
@ -49,10 +49,10 @@ module.exports = {
const themeName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
const backupName = `${themeName}_${ObjectID()}`;
// check if zip name is casper.zip
if (zip.name === 'casper.zip') {
// check if zip name matches one of the default themes
if (zip.name === 'casper.zip' || zip.name === 'source.zip') {
throw new errors.ValidationError({
message: tpl(messages.overrideCasper)
message: tpl(messages.overrideDefaultTheme)
});
}
@ -127,9 +127,9 @@ module.exports = {
}
},
destroy: async function (themeName) {
if (themeName === 'casper') {
if (themeName === 'casper' || themeName === 'source') {
throw new errors.ValidationError({
message: tpl(messages.destroyCasper)
message: tpl(messages.destroyDefaultTheme)
});
}

View File

@ -1,2 +1,3 @@
test/coverage/**
test/utils/fixtures/themes/casper/assets/**
test/utils/fixtures/themes/source/assets/**

View File

@ -98,7 +98,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -367,7 +367,7 @@ Object {
"settings": Array [
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
],
}
@ -508,7 +508,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -866,7 +866,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -1223,7 +1223,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -1585,7 +1585,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -2035,7 +2035,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",
@ -2457,7 +2457,7 @@ Object {
},
Object {
"key": "active_theme",
"value": "casper",
"value": "source",
},
Object {
"key": "is_private",

View File

@ -45,17 +45,17 @@ describe('Themes API', function () {
const jsonResponse = res.body;
should.exist(jsonResponse.themes);
localUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(6);
jsonResponse.themes.length.should.eql(7);
localUtils.API.checkResponse(jsonResponse.themes[0], 'theme');
jsonResponse.themes[0].name.should.eql('broken-theme');
jsonResponse.themes[0].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[0].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[1], 'theme', 'templates');
localUtils.API.checkResponse(jsonResponse.themes[1], 'theme');
jsonResponse.themes[1].name.should.eql('casper');
jsonResponse.themes[1].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[1].active.should.be.true();
jsonResponse.themes[1].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[2], 'theme');
jsonResponse.themes[2].name.should.eql('locale-theme');
@ -67,15 +67,20 @@ describe('Themes API', function () {
jsonResponse.themes[3].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[3].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[4], 'theme');
jsonResponse.themes[4].name.should.eql('test-theme');
localUtils.API.checkResponse(jsonResponse.themes[4], 'theme', 'templates');
jsonResponse.themes[4].name.should.eql('source');
jsonResponse.themes[4].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[4].active.should.be.false();
jsonResponse.themes[4].active.should.be.true();
localUtils.API.checkResponse(jsonResponse.themes[5], 'theme');
jsonResponse.themes[5].name.should.eql('test-theme-channels');
jsonResponse.themes[5].package.should.be.false();
jsonResponse.themes[5].name.should.eql('test-theme');
jsonResponse.themes[5].package.should.be.an.Object().with.properties('name', 'version');
jsonResponse.themes[5].active.should.be.false();
localUtils.API.checkResponse(jsonResponse.themes[6], 'theme');
jsonResponse.themes[6].name.should.eql('test-theme-channels');
jsonResponse.themes[6].package.should.be.false();
jsonResponse.themes[6].active.should.be.false();
});
it('Can download a theme', async function () {
@ -129,13 +134,13 @@ describe('Themes API', function () {
should.exist(jsonResponse3.themes);
localUtils.API.checkResponse(jsonResponse3, 'themes');
jsonResponse3.themes.length.should.eql(7);
jsonResponse3.themes.length.should.eql(8);
// Casper should be present and still active
const casperTheme = _.find(jsonResponse3.themes, {name: 'casper'});
should.exist(casperTheme);
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
casperTheme.active.should.be.true();
// Source should be present and still active
const sourceTheme = _.find(jsonResponse3.themes, {name: 'source'});
should.exist(sourceTheme);
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
sourceTheme.active.should.be.true();
// The added theme should be here
const addedTheme = _.find(jsonResponse3.themes, {name: 'valid'});
@ -149,6 +154,7 @@ describe('Themes API', function () {
'casper',
'locale-theme',
'members-test-theme',
'source',
'test-theme',
'test-theme-channels',
'valid'
@ -171,7 +177,7 @@ describe('Themes API', function () {
tmpFolderContents.splice(i, 1);
}
}
tmpFolderContents.should.be.an.Array().with.lengthOf(10);
tmpFolderContents.should.be.an.Array().with.lengthOf(11);
tmpFolderContents.should.eql([
'broken-theme',
@ -180,6 +186,7 @@ describe('Themes API', function () {
'invalid.zip',
'locale-theme',
'members-test-theme',
'source',
'test-theme',
'test-theme-channels',
'valid.zip',
@ -196,13 +203,13 @@ describe('Themes API', function () {
should.exist(jsonResponse2.themes);
localUtils.API.checkResponse(jsonResponse2, 'themes');
jsonResponse2.themes.length.should.eql(6);
jsonResponse2.themes.length.should.eql(7);
// Casper should be present and still active
const casperTheme = _.find(jsonResponse2.themes, {name: 'casper'});
should.exist(casperTheme);
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
casperTheme.active.should.be.true();
// Source should be present and still active
const sourceTheme = _.find(jsonResponse2.themes, {name: 'source'});
should.exist(sourceTheme);
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
sourceTheme.active.should.be.true();
// The deleted theme should not be here
const deletedTheme = _.find(jsonResponse2.themes, {name: 'valid'});
@ -238,12 +245,12 @@ describe('Themes API', function () {
should.exist(jsonResponse.themes);
localUtils.API.checkResponse(jsonResponse, 'themes');
jsonResponse.themes.length.should.eql(6);
jsonResponse.themes.length.should.eql(7);
const casperTheme = _.find(jsonResponse.themes, {name: 'casper'});
should.exist(casperTheme);
localUtils.API.checkResponse(casperTheme, 'theme', 'templates');
casperTheme.active.should.be.true();
const sourceTheme = _.find(jsonResponse.themes, {name: 'source'});
should.exist(sourceTheme);
localUtils.API.checkResponse(sourceTheme, 'theme', 'templates');
sourceTheme.active.should.be.true();
const testTheme = _.find(jsonResponse.themes, {name: 'test-theme'});
should.exist(testTheme);
@ -263,8 +270,8 @@ describe('Themes API', function () {
localUtils.API.checkResponse(jsonResponse2, 'themes');
jsonResponse2.themes.length.should.eql(1);
const casperTheme2 = _.find(jsonResponse2.themes, {name: 'casper'});
should.not.exist(casperTheme2);
const sourceTheme2 = _.find(jsonResponse2.themes, {name: 'source'});
should.not.exist(sourceTheme2);
const testTheme2 = _.find(jsonResponse2.themes, {name: 'test-theme'});
should.exist(testTheme2);
@ -318,7 +325,7 @@ describe('Themes API', function () {
});
it('Can re-upload the active theme to override', async function () {
// The tricky thing about this test is the default active theme is Casper and you're not allowed to override it.
// The tricky thing about this test is the default active theme is Source and you're not allowed to override it.
// So we upload a valid theme, activate it, and then upload again.
sinon.stub(settingsCache, 'get').callsFake(function (key, options) {
if (key === 'active_theme') {

View File

@ -68,7 +68,6 @@ describe('Default Frontend routing', function () {
$('body.home-template').length.should.equal(1);
$('article.post').length.should.equal(7);
$('article.tag-getting-started').length.should.equal(7);
res.text.should.not.containEql('__GHOST_URL__');
});

View File

@ -114,7 +114,7 @@ describe('Importer', function () {
return models.Settings.findOne(_.merge({key: 'active_theme'}, testUtils.context.internal));
})
.then(function (result) {
result.attributes.value.should.eql('casper');
result.attributes.value.should.eql('source');
});
});

View File

@ -195,7 +195,7 @@ describe('Authentication API', function () {
const password = 'thisissupersafe';
const requestMock = nock('https://api.github.com')
.get('/repos/tryghost/casper/zipball')
.get('/repos/tryghost/source/zipball')
.query(true)
.replyWithFile(200, fixtureManager.getPathForFixture('themes/valid.zip'));
@ -207,7 +207,7 @@ describe('Authentication API', function () {
email,
password,
blogTitle: 'a test blog',
theme: 'TryGhost/Casper',
theme: 'TryGhost/Source',
accentColor: '#85FF00',
description: 'Custom Site Description on Setup &mdash; great for everyone'
}]
@ -236,7 +236,7 @@ describe('Authentication API', function () {
const activeTheme = await settingsCache.get('active_theme');
const accentColor = await settingsCache.get('accent_color');
const description = await settingsCache.get('description');
assert.equal(activeTheme, 'casper', 'The theme casper should have been installed');
assert.equal(activeTheme, 'source', 'The theme Source should have been installed');
assert.equal(accentColor, '#85FF00', 'The accent color should have been set');
assert.equal(description, 'Custom Site Description on Setup &mdash; great for everyone', 'The site description should have been set');

View File

@ -65,8 +65,7 @@ describe('Dynamic Routing', function () {
$('title').text().should.equal('Ghost');
$('body.home-template').length.should.equal(1);
$('article.post').length.should.equal(5);
$('article.tag-getting-started').length.should.equal(5);
$('article.post').length.should.equal(7);
done();
});
@ -142,9 +141,8 @@ describe('Dynamic Routing', function () {
should.not.exist(res.headers['set-cookie']);
should.exist(res.headers.date);
$('body').attr('class').should.eql('tag-template tag-getting-started');
$('body').attr('class').should.eql('tag-template tag-getting-started has-sans-title has-sans-body');
$('article.post').length.should.equal(5);
$('article.tag-getting-started').length.should.equal(5);
done();
});

View File

@ -37,7 +37,7 @@ describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = '1b75aae9befefea53b17c9c1991c8a1d';
const currentFixturesHash = '4db87173699ad9c9d8a67ccab96dfd2d';
const currentSettingsHash = '3a7ca0aa6a06cba47e3e898aef7029c2';
const currentSettingsHash = '3128d4ec667a50049486b0c21f04be07';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
// If this test is failing, then it is likely a change has been made that requires a DB version bump,

View File

@ -124,7 +124,7 @@ const prepareContentFolder = async ({contentFolder, redirectsFile = true, routes
await fs.ensureDir(path.join(contentFolderForTests, 'adapters'));
await fs.ensureDir(path.join(contentFolderForTests, 'settings'));
// Copy all themes into the new test content folder. Default active theme is always casper.
// Copy all themes into the new test content folder. Default active theme is always source.
// If you want to use a different theme, you have to set the active theme (e.g. stub)
await fs.copy(
path.join(__dirname, 'fixtures', 'themes'),

View File

@ -75,12 +75,12 @@ const prepareContentFolder = (options) => {
fs.ensureDirSync(path.join(contentFolderForTests, 'settings'));
if (options.copyThemes) {
// Copy all themes into the new test content folder. Default active theme is always casper. If you want to use a different theme, you have to set the active theme (e.g. stub)
// Copy all themes into the new test content folder. Default active theme is always source. If you want to use a different theme, you have to set the active theme (e.g. stub)
fs.copySync(path.join(__dirname, 'fixtures', 'themes'), path.join(contentFolderForTests, 'themes'));
}
// Copy theme even if frontend is disabled, as admin can use casper when viewing themes section
fs.copySync(path.join(__dirname, 'fixtures', 'themes', 'casper'), path.join(contentFolderForTests, 'themes', 'casper'));
// Copy theme even if frontend is disabled, as admin can use source when viewing themes section
fs.copySync(path.join(__dirname, 'fixtures', 'themes', 'source'), path.join(contentFolderForTests, 'themes', 'source'));
if (options.redirectsFile) {
redirects.setupFile(contentFolderForTests, options.redirectsFileExt);

View File

@ -212,7 +212,7 @@
},
"theme": {
"active_theme": {
"defaultValue": "casper",
"defaultValue": "source",
"flags": "RO",
"type": "string"
}

View File

@ -224,7 +224,7 @@
},
"theme": {
"active_theme": {
"defaultValue": "casper",
"defaultValue": "source",
"flags": "RO",
"type": "string"
}

View File

@ -21,6 +21,25 @@ To update it:
- `rm -rf casper`
- `rsync -rv --exclude '.git*' --exclude 'assets/css*' --exclude 'assets/js*' --exclude 'gulpfile.js' --exclude 'yarn.lock' --exclude 'README.md' ../../../../content/themes/casper .`
## Updating the Source theme fixture
The source fixture is a partial copy of the content/themes/source folder.
It should not include any files that aren't needed to run the theme.
To update it:
1. Ensure your `content/themes/source` folder is on the latest released version e.g.
- Run `yarn main`
- `cd content/themes/source`
- `git log -20` - find the latest tag
- `git checkout vx.y.z` - checkout the latest tag
2. Ensure you are in side this folder (the fixtures/themes directory), remove source entirely and then copy it across fresh:
- `cd tests/utils/fixtures/themes`
- `rm -rf source`
- `rsync -rv --exclude '.git*' --exclude 'assets/css*' --exclude 'assets/js*' --exclude 'gulpfile.js' --exclude 'yarn.lock' --exclude 'README.md' ../../../../content/themes/source .`
### Modifying theme fixtures
When a new rule is introduced in gscan one of these fixture files might break and you'll have to update a "zip" which isn't as easy as opening a text editor... It could become that one day but for now here are some commands to help out with the edit process

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

View File

@ -0,0 +1 @@
<svg width="264" height="88" viewBox="0 0 264 88" xmlns="http://www.w3.org/2000/svg"><title>default-skin 2</title><g fill="none" fill-rule="evenodd"><g><path d="M67.002 59.5v3.768c-6.307.84-9.184 5.75-10.002 9.732 2.22-2.83 5.564-5.098 10.002-5.098V71.5L73 65.585 67.002 59.5z" id="Shape" fill="#fff"/><g fill="#fff"><path d="M13 29v-5h2v3h3v2h-5zM13 15h5v2h-3v3h-2v-5zM31 15v5h-2v-3h-3v-2h5zM31 29h-5v-2h3v-3h2v5z" id="Shape"/></g><g fill="#fff"><path d="M62 24v5h-2v-3h-3v-2h5zM62 20h-5v-2h3v-3h2v5zM70 20v-5h2v3h3v2h-5zM70 24h5v2h-3v3h-2v-5z"/></g><path d="M20.586 66l-5.656-5.656 1.414-1.414L22 64.586l5.656-5.656 1.414 1.414L23.414 66l5.656 5.656-1.414 1.414L22 67.414l-5.656 5.656-1.414-1.414L20.586 66z" fill="#fff"/><path d="M111.785 65.03L110 63.5l3-3.5h-10v-2h10l-3-3.5 1.785-1.468L117 59l-5.215 6.03z" fill="#fff"/><path d="M152.215 65.03L154 63.5l-3-3.5h10v-2h-10l3-3.5-1.785-1.468L147 59l5.215 6.03z" fill="#fff"/><g><path id="Rectangle-11" fill="#fff" d="M160.957 28.543l-3.25-3.25-1.413 1.414 3.25 3.25z"/><path d="M152.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" id="Oval-1" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M150 21h5v1h-5z"/></g><g><path d="M116.957 28.543l-1.414 1.414-3.25-3.25 1.414-1.414 3.25 3.25z" fill="#fff"/><path d="M108.5 27c3.038 0 5.5-2.462 5.5-5.5s-2.462-5.5-5.5-5.5-5.5 2.462-5.5 5.5 2.462 5.5 5.5 5.5z" stroke="#fff" stroke-width="1.5"/><path fill="#fff" d="M106 21h5v1h-5z"/><path fill="#fff" d="M109.043 19.008l-.085 5-1-.017.085-5z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

View File

@ -0,0 +1,43 @@
{{!< default}}
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
<main class="gh-main gh-outer">
{{#author}}
<section class="gh-archive{{#if @custom.show_site_in_sidebar}} has-sidebar{{/if}} gh-inner">
<div class="gh-archive-inner">
<div class="gh-archive-wrapper">
<h1 class="gh-article-title is-title">
{{#if website}}
<a class="gh-author-social-link" href="{{website}}" target="_blank" rel="noopener">{{name}}</a>
{{else}}
{{name}}
{{/if}}
</h1>
{{#if bio}}
<p class="gh-article-excerpt">{{bio}}</p>
{{/if}}
<footer class="gh-author-meta">
<div class="gh-author-social">
{{#if facebook}}
<a class="gh-author-social-link" href="{{facebook_url}}" target="_blank" rel="noopener">{{> "icons/facebook"}}</a>
{{/if}}
{{#if twitter}}
<a class="gh-author-social-link" href="{{twitter_url}}" target="_blank" rel="noopener">{{> "icons/twitter"}}</a>
{{/if}}
</div>
{{#if location}}
<div class="gh-author-location">{{location}}</div>
{{/if}}
</footer>
</div>
{{#if profile_image}}
<img class="gh-article-image" src="{{img_url profile_image size="s"}}" alt="{{name}}">
{{/if}}
</div>
</section>
{{/author}}
{{> "components/post-list" feed="archive" postFeedStyle=@custom.post_feed_style showTitle=false showSidebar=@custom.show_site_in_sidebar}}
</main>

View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="{{@site.locale}}">
<head>
{{!-- Basic meta - advanced meta is output with {{ghost_head}} below --}}
<title>{{meta_title}}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{!-- Preload main styles and scripts for better performance --}}
<link rel="preload" as="style" href="{{asset "built/screen.css"}}">
<link rel="preload" as="script" href="{{asset "built/source.js"}}">
{{!-- Theme assets - use the {{asset}} helper to reference styles & scripts, this will take care of caching and cache-busting automatically --}}
<link rel="stylesheet" type="text/css" href="{{asset "built/screen.css"}}">
{{!-- Custom background color --}}
<style>
:root {
--background-color: {{@custom.site_background_color}}
}
</style>
<script src="http://localhost:35729/livereload.js?snipver=1"></script>
<script>
/* The script for calculating the color contrast has been taken from
https://gomakethings.com/dynamically-changing-the-text-color-based-on-background-color-contrast-with-vanilla-js/ */
var accentColor = getComputedStyle(document.documentElement).getPropertyValue('--background-color');
accentColor = accentColor.trim().slice(1);
var r = parseInt(accentColor.substr(0, 2), 16);
var g = parseInt(accentColor.substr(2, 2), 16);
var b = parseInt(accentColor.substr(4, 2), 16);
var yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000;
var textColor = (yiq >= 128) ? 'dark' : 'light';
document.documentElement.className = `has-${textColor}-text`;
</script>
{{!-- This tag outputs all your advanced SEO meta, structured data, and other important settings, it should always be the last tag before the closing head tag --}}
{{ghost_head}}
</head>
<body class="{{body_class}} has-{{#match @custom.title_font "Elegant serif"}}serif{{else match @custom.title_font "Clean slab"}}slab{{else}}sans{{/match}}-title has-{{#match @custom.body_font "Elegant serif"}}serif{{else}}sans{{/match}}-body">
<div class="gh-viewport">
{{> "components/navigation" navigationLayout=@custom.navigation_layout}}
{{{body}}}
{{> "components/footer"}}
</div>
{{#is "post, page"}}
{{> "lightbox"}}
{{/is}}
{{!-- Scripts - handle responsive videos, infinite scroll, and navigation dropdowns --}}
<script src="{{asset "built/source.js"}}"></script>
{{!-- Ghost outputs required functional scripts with this tag, it should always be the last thing before the closing body tag --}}
{{ghost_foot}}
</body>
</html>

View File

@ -0,0 +1,12 @@
{{!< default}}
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
{{> "components/header" headerStyle=@custom.header_style}}
{{#match @custom.header_style "!=" "Highlight"}}
{{> "components/featured" showFeatured=@custom.highlight_featured_posts limit=4}}
{{/match}}
{{> "components/cta"}}
{{> "components/post-list" feed="home" postFeedStyle=@custom.post_feed_style showTitle=true showSidebar=@custom.show_site_in_sidebar}}

View File

@ -0,0 +1,6 @@
{{!< default}}
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
<main class="gh-main">
{{> "components/post-list" feed="index" postFeedStyle=@custom.post_feed_style showTitle=true showSidebar=@custom.show_site_in_sidebar}}
</main>

View File

@ -0,0 +1,199 @@
{
"name": "source",
"description": "A default theme for the Ghost publishing platform",
"demo": "https://demo.ghost.io",
"version": "6.0.0",
"engines": {
"ghost": ">=5.0.0"
},
"license": "MIT",
"scripts": {
"dev": "gulp",
"zip": "gulp zip",
"test": "gscan .",
"test:ci": "gscan --fatal --verbose .",
"pretest": "gulp build",
"preship": "yarn test",
"ship": "STATUS=$(git status --porcelain); echo $STATUS; if [ -z \"$STATUS\" ]; then yarn version && git push --follow-tags; else echo \"Uncomitted changes found.\" && exit 1; fi",
"postship": "git fetch && gulp release"
},
"author": {
"name": "Ghost Foundation",
"email": "hello@ghost.org",
"url": "https://ghost.org/"
},
"gpm": {
"type": "theme",
"categories": [
"Minimal",
"Magazine"
]
},
"keywords": [
"ghost",
"theme",
"ghost-theme"
],
"repository": {
"type": "git",
"url": "https://github.com/TryGhost/Source.git"
},
"bugs": "https://github.com/TryGhost/Source/issues",
"contributors": "https://github.com/TryGhost/Source/graphs/contributors",
"devDependencies": {
"@tryghost/release-utils": "0.8.1",
"autoprefixer": "10.4.7",
"beeper": "2.1.0",
"cssnano": "5.1.12",
"gscan": "4.36.1",
"gulp": "4.0.2",
"gulp-concat": "2.6.1",
"gulp-livereload": "4.0.2",
"gulp-postcss": "9.0.1",
"gulp-uglify": "3.0.2",
"gulp-zip": "5.1.0",
"inquirer": "8.2.4",
"postcss": "8.2.13",
"postcss-easy-import": "4.0.0",
"pump": "3.0.0"
},
"browserslist": [
"defaults"
],
"config": {
"posts_per_page": 16,
"image_sizes": {
"xxs": {
"width": 30
},
"xs": {
"width": 100
},
"s": {
"width": 300
},
"m": {
"width": 600
},
"l": {
"width": 1000
},
"xl": {
"width": 2000
}
},
"card_assets": true,
"custom": {
"navigation_layout": {
"type": "select",
"options": [
"Logo in the middle",
"Logo on the left",
"Stacked"
],
"default": "Logo in the middle"
},
"site_background_color": {
"type": "color",
"default": "#ffffff"
},
"header_and_footer_color": {
"type": "select",
"options": [
"Accent color",
"Background color"
],
"default": "Accent color"
},
"title_font": {
"type": "select",
"options": [
"Modern sans-serif",
"Elegant serif",
"Clean slab"
],
"default": "Modern sans-serif"
},
"body_font": {
"type": "select",
"options": [
"Modern sans-serif",
"Elegant serif"
],
"default": "Modern sans-serif"
},
"signup_heading": {
"type": "text",
"description": "Used in your footer across your theme, defaults to site title when empty"
},
"signup_subheading": {
"type": "text",
"description": "Defaults to site description when empty"
},
"header_style": {
"type": "select",
"options": [
"Landing",
"Highlight",
"Magazine",
"Search",
"Off"
],
"description": "Highlight & Magazine styles will default to Landing until 7 posts have been published",
"default": "Landing",
"group": "homepage"
},
"use_publication_cover_as_background": {
"type": "boolean",
"default": false,
"group": "homepage",
"visibility": "header_style:[Landing, Search]"
},
"highlight_featured_posts": {
"type": "boolean",
"default": false,
"group": "homepage",
"visibility": "header_style:[Highlight, Magazine]"
},
"post_feed_style": {
"type": "select",
"options": [
"List",
"Grid"
],
"default": "List",
"group": "homepage"
},
"show_images_in_feed": {
"type": "boolean",
"default": true,
"description": "Toggles thumbnails of the post cards when the post feed style is List",
"group": "homepage",
"visibility": "post_feed_style:List"
},
"show_author": {
"type": "boolean",
"default": true,
"description": "Show the author below each post",
"group": "homepage"
},
"show_publish_date": {
"type": "boolean",
"default": true,
"description": "Show the date published below each post",
"group": "homepage"
},
"show_site_in_sidebar": {
"type": "boolean",
"default": false,
"description": "Include your site info on the side of the post feed",
"group": "homepage"
}
}
},
"renovate": {
"extends": [
"@tryghost:theme"
]
}
}

View File

@ -0,0 +1,26 @@
{{!< default}}
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
{{#post}}
<main class="gh-main">
<article class="gh-article {{post_class}}">
{{#match @page.show_title_and_feature_image}}
<header class="gh-article-header gh-canvas">
<h1 class="gh-article-title is-title">{{title}}</h1>
{{#if custom_excerpt}}
<p class="gh-article-excerpt is-body">{{custom_excerpt}}</p>
{{/if}}
{{> "feature-image"}}
</header>
{{/match}}
<section class="gh-content gh-canvas is-body">
{{content}}
</section>
</article>
</main>
{{/post}}

View File

@ -0,0 +1,25 @@
{{#if @site.members_enabled}}
{{#unless @member}}
{{#match @custom.header_style "!=" "Landing"}}
{{#match @custom.header_style "!=" "Search"}}
{{#match @custom.header_style "!=" "Off"}}
{{#match posts.length ">=" 7}}
<section class="gh-cta gh-outer">
<div class="gh-cta-inner gh-inner">
<div class="gh-cta-content">
<h2 class="gh-cta-title is-title">
{{#if @custom.signup_heading}}{{@custom.signup_heading}}{{else}}{{@site.title}}{{/if}}
</h2>
<p class="gh-cta-description is-body">
{{#if @custom.signup_subheading}}{{@custom.signup_subheading}}{{else}}{{@site.description}}{{/if}}
</p>
</div>
{{> "email-subscription"}}
</div>
</section>
{{/match}}
{{/match}}
{{/match}}
{{/match}}
{{/unless}}
{{/if}}

View File

@ -0,0 +1,14 @@
{{#if showFeatured}}
{{#get "posts" filter="featured:true" include="authors" limit=limit as |featured|}}
<section class="gh-featured gh-outer">
<div class="gh-featured-inner gh-inner">
<h2 class="gh-featured-title">Featured</h2>
<div class="gh-featured-feed">
{{#foreach featured}}
{{> "post-card"}}
{{/foreach}}
</div>
</div>
</section>
{{/get}}
{{/if}}

View File

@ -0,0 +1,35 @@
<footer class="gh-footer{{#match @custom.header_and_footer_color "Accent color"}} has-accent-color{{/match}} gh-outer">
<div class="gh-footer-inner gh-inner">
<div class="gh-footer-bar">
<span class="gh-footer-logo is-title">
{{#if @site.logo}}
<img src="{{@site.logo}}" alt="{{@site.title}}">
{{else}}
{{@site.title}}
{{/if}}
</span>
<nav class="gh-footer-menu">
{{navigation type="secondary"}}
</nav>
<div class="gh-footer-copyright">
Powered by <a href="https://ghost.org/" target="_blank" rel="noopener">Ghost</a>
</div>
</div>
{{#if @site.members_enabled}}
{{#unless @member}}
<section class="gh-footer-signup">
<h2 class="gh-footer-signup-header is-title">
{{#if @custom.signup_heading}}{{@custom.signup_heading}}{{else}}{{@site.title}}{{/if}}
</h2>
<p class="gh-footer-signup-subhead is-body">
{{#if @custom.signup_subheading}}{{@custom.signup_subheading}}{{else}}{{@site.description}}{{/if}}
</p>
{{> "email-subscription"}}
</section>
{{/unless}}
{{/if}}
</div>
</footer>

View File

@ -0,0 +1,77 @@
<section class="gh-header is-{{#match headerStyle "Magazine"}}magazine{{else match headerStyle "Highlight"}}highlight{{else}}classic{{/match}}{{#if @custom.use_publication_cover_as_background}}{{#if @site.cover_image}} has-image{{/if}}{{/if}} gh-outer">
{{!-- Background image --}}
{{#if @custom.use_publication_cover_as_background}}
{{#match headerStyle "!=" "Magazine"}}
{{#match headerStyle "!=" "Highlight"}}
{{#if @site.cover_image}}
<img class="gh-header-image" src="{{@site.cover_image}}" alt="{{@site.title}}">
{{/if}}
{{/match}}
{{/match}}
{{/if}}
<div class="gh-header-inner gh-inner">
{{!-- Highlight layout --}}
{{#match headerStyle "Highlight"}}
<div class="gh-header-left">
{{#foreach posts limit="1"}}
{{> "post-card"}}
{{/foreach}}
</div>
<div class="gh-header-middle">
{{#foreach posts from="2" limit="3"}}
{{> "post-card"}}
{{/foreach}}
</div>
<div class="gh-header-right">
{{#if @custom.highlight_featured_posts}}
{{> "components/featured" showFeatured=@custom.highlight_featured_posts limit=6}}
{{else}}
<div class="gh-featured-feed">
{{#foreach posts from="5" limit="6"}}
{{> "post-card"}}
{{/foreach}}
</div>
{{/if}}
</div>
{{/match}}
{{!-- Magazine layout --}}
{{#match headerStyle "Magazine"}}
{{#foreach posts limit="7"}}
{{#match @number 2}}
<div class="gh-header-left">
{{/match}}
{{#match @number 5}}
<div class="gh-header-right">
{{/match}}
{{> "post-card"}}
{{#match @number 4}}
</div>
{{/match}}
{{#match @number 7}}
</div>
{{/match}}
{{/foreach}}
{{/match}}
{{!-- Landing layout --}}
{{#match headerStyle "Landing"}}
<h1 class="gh-header-title is-title">{{@site.description}}</h1>
{{> "email-subscription"}}
{{/match}}
{{!-- Search layout --}}
{{#match headerStyle "Search"}}
<h1 class="gh-header-title is-title">{{@site.description}}</h1>
<form class="gh-form">
{{> "icons/search"}}
<button class="gh-form-input" data-ghost-search>Search posts, tags and authors</button>
</form>
{{/match}}
</div>
</section>

View File

@ -0,0 +1,23 @@
{{#match headerStyle "!=" "Off"}}
{{#match headerStyle "Highlight"}}
{{#match posts.length ">=" 7}}
{{> "components/header-content"}}
{{/match}}
{{else match headerStyle "Magazine"}}
{{#match posts.length ">=" 7}}
{{> "components/header-content"}}
{{/match}}
{{else}}
{{#match headerStyle "Landing"}}
{{#if @site.members_enabled}}
{{#unless @member}}
{{> "components/header-content"}}
{{/unless}}
{{/if}}
{{else}}
{{> "components/header-content"}}
{{/match}}
{{/match}}
{{/match}}

View File

@ -0,0 +1,53 @@
<header id="gh-navigation" class="gh-navigation is-{{#match navigationLayout "Logo on the left"}}left-logo{{else match navigationLayout "Stacked"}}stacked{{else}}middle-logo{{/match}}{{#match @custom.header_and_footer_color "Accent color"}} has-accent-color{{/match}} gh-outer">
<div class="gh-navigation-inner gh-inner">
<div class="gh-navigation-brand">
<a class="gh-navigation-logo is-title" href="{{@site.url}}">
{{#if @site.logo}}
<img src="{{@site.logo}}" alt="{{@site.title}}">
{{else}}
{{@site.title}}
{{/if}}
</a>
{{> "search-toggle"}}
<button class="gh-burger gh-icon-button">
{{> "icons/burger"}}
{{> "icons/close"}}
</button>
</div>
<nav class="gh-navigation-menu">
{{navigation}}
{{#unless @site.members_enabled}}
{{#match navigationLayout "Stacked"}}
{{> "search-toggle"}}
{{/match}}
{{/unless}}
</nav>
<div class="gh-navigation-actions">
{{#unless @site.members_enabled}}
{{^match navigationLayout "Stacked"}}
{{> "search-toggle"}}
{{/match}}
{{else}}
{{> "search-toggle"}}
<div class="gh-navigation-members">
{{#unless @member}}
{{#unless @site.members_invite_only}}
<a href="#/portal/signin" data-portal="signin">Sign in</a>
{{#unless hideSubscribeButton}}
<a class="gh-button" href="#/portal/signup" data-portal="signup">Subscribe</a>
{{/unless}}
{{else}}
<a class="gh-button" href="#/portal/signin" data-portal="signin">Sign in</a>
{{/unless}}
{{else}}
<a class="gh-button" href="#/portal/account" data-portal="account">Account</a>
{{/unless}}
</div>
{{/unless}}
</div>
</div>
</header>

View File

@ -0,0 +1,133 @@
{{!--
Parameters:
* feed (index, home, archive, recent)
* postFeedStyle (list, grid)
* showTitle (true, false)
* showSidebar (true, false)
--}}
<section class="gh-container is-{{#match postFeedStyle "List"}}list{{else}}grid{{/match}}{{#if showSidebar}} has-sidebar{{/if}}{{#unless @custom.show_images_in_feed}} no-image{{/unless}} gh-outer">
<div class="gh-container-inner gh-inner">
{{#if showTitle}}
<h2 class="gh-container-title">
{{#unless title}}Latest{{else}}{{title}}{{/unless}}
</h2>
{{/if}}
<main class="gh-main">
<div class="gh-feed">
{{!-- Homepage --}}
{{#match feed "home"}}
{{#match @custom.header_style "Highlight"}}
{{#match posts.length ">=" 7}}
{{#if @custom.highlight_featured_posts}}
{{#get "posts" include="authors" limit="16"}}
{{#foreach posts from="5" limit="12"}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{else}}
{{#get "posts" include="authors" limit="22"}}
{{#foreach posts from="11" limit="12"}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{/if}}
{{else}}
{{#get "posts" include="authors" limit="12"}}
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{/match}}
{{else match @custom.header_style "Magazine"}}
{{#match posts.length ">=" 7}}
{{#get "posts" include="authors" limit="19"}}
{{#foreach posts from="8" limit="12"}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{else}}
{{#get "posts" include="authors" limit="12"}}
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{/match}}
{{else}}
{{#get "posts" include="authors" limit="12"}}
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{/match}}
{{/match}}
{{!-- All posts --}}
{{#match feed "index"}}
{{#match pagination.page 2}}
{{#get "posts" include="authors" limit=@config.posts_per_page as |recent|}}
{{#foreach recent}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{/match}}
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
{{/match}}
{{!-- Tag and author pages --}}
{{#match feed "archive"}}
{{#foreach posts}}
{{> "post-card"}}
{{/foreach}}
{{/match}}
{{!-- Recent posts --}}
{{#match feed "recent"}}
{{#get "posts" include="authors" filter="id:-{{post.id}}" limit="4" as |next|}}
{{#foreach next}}
{{> "post-card"}}
{{/foreach}}
{{/get}}
{{/match}}
</div>
</main>
{{#if showSidebar}}
<aside class="gh-sidebar">
<section class="gh-about">
{{#if @site.icon}}
<img class="gh-about-icon" src="{{@site.icon}}" alt="{{@site.title}}">
{{/if}}
<h3 class="gh-about-title is-title">{{@site.title}}</h3>
{{#if @site.description}}
<p class="gh-about-description is-body">{{@site.description}}</p>
{{/if}}
{{#if @site.members_enabled}}
{{#unless @member}}
<button class="gh-button" data-portal="signup">Subscribe</button>
{{else}}
{{#if @site.paid_members_enabled}}
{{#unless @member.paid}}
<button class="gh-button" data-portal="upgrade">Upgrade</button>
{{/unless}}
{{/if}}
{{/unless}}
{{/if}}
</section>
</aside>
{{/if}}
{{#match pagination.pages ">" 1}}
<div class="gh-more is-title">
<a href="/page/2">See all {{> "icons/arrow"}}</a>
</div>
{{/match}}
</div>
</section>

View File

@ -0,0 +1,8 @@
<form class="gh-form" data-members-form>
<input class="gh-form-input" type="email" placeholder="jamie@example.com" required data-members-email>
<button class="gh-button" type="submit">
<span><span>Subscribe</span> {{> "icons/arrow"}}</span>
{{> "icons/loader"}}
{{> "icons/checkmark"}}
</button>
</form>

View File

@ -0,0 +1,17 @@
{{#if feature_image}}
<figure class="gh-article-image">
<img
srcset="{{img_url feature_image size="s"}} 300w,
{{img_url feature_image size="m"}} 720w,
{{img_url feature_image size="l"}} 960w,
{{img_url feature_image size="xl"}} 1200w,
{{img_url feature_image size="xxl"}} 2000w"
sizes="(max-width: 1200px) 100vw, 1200px"
src="{{img_url feature_image size="xl"}}"
alt="{{title}}"
>
{{#if feature_image_caption}}
<figcaption>{{feature_image_caption}}</figcaption>
{{/if}}
</figure>
{{/if}}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M224.49,136.49l-72,72a12,12,0,0,1-17-17L187,140H40a12,12,0,0,1,0-24H187L135.51,64.48a12,12,0,0,1,17-17l72,72A12,12,0,0,1,224.49,136.49Z"></path></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M3.513 18.998C4.749 15.504 8.082 13 12 13s7.251 2.504 8.487 5.998C18.47 21.442 15.417 23 12 23s-6.47-1.558-8.487-4.002zM12 12c2.21 0 4-2.79 4-5s-1.79-4-4-4-4 1.79-4 4 1.79 5 4 5z" fill="#FFF"/></g></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z"></path></svg>

After

Width:  |  Height:  |  Size: 282 B

View File

@ -0,0 +1,24 @@
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
<style>
.checkmark {
width: 40px;
height: 40px;
display: block;
stroke-width: 2.5;
stroke: currentColor;
stroke-miterlimit: 10;
}
.checkmark__check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke .3s cubic-bezier(0.650, 0.000, 0.450, 1.000) forwards;
}
@keyframes stroke {
100% { stroke-dashoffset: 0; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 716 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 256 256"><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path></svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@ -0,0 +1 @@
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M23.9981 11.9991C23.9981 5.37216 18.626 0 11.9991 0C5.37216 0 0 5.37216 0 11.9991C0 17.9882 4.38789 22.9522 10.1242 23.8524V15.4676H7.07758V11.9991H10.1242V9.35553C10.1242 6.34826 11.9156 4.68714 14.6564 4.68714C15.9692 4.68714 17.3424 4.92149 17.3424 4.92149V7.87439H15.8294C14.3388 7.87439 13.8739 8.79933 13.8739 9.74824V11.9991H17.2018L16.6698 15.4676H13.8739V23.8524C19.6103 22.9522 23.9981 17.9882 23.9981 11.9991Z"/></svg>

After

Width:  |  Height:  |  Size: 531 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.49365 4.58752C3.53115 6.03752 2.74365 7.70002 2.74365 9.25002C2.74365 10.6424 3.29678 11.9778 4.28134 12.9623C5.26591 13.9469 6.60127 14.5 7.99365 14.5C9.38604 14.5 10.7214 13.9469 11.706 12.9623C12.6905 11.9778 13.2437 10.6424 13.2437 9.25002C13.2437 6.00002 10.9937 3.50002 9.16865 1.68127L6.99365 6.25002L4.49365 4.58752Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 24 24">
<g stroke-linecap="round" stroke-width="2" fill="currentColor" stroke="none" stroke-linejoin="round" class="nc-icon-wrapper">
<g class="nc-loop-dots-4-24-icon-o">
<circle cx="4" cy="12" r="3"></circle>
<circle cx="12" cy="12" r="3"></circle>
<circle cx="20" cy="12" r="3"></circle>
</g>
<style data-cap="butt">
.nc-loop-dots-4-24-icon-o{--animation-duration:0.8s}
.nc-loop-dots-4-24-icon-o *{opacity:.4;transform:scale(.75);animation:nc-loop-dots-4-anim var(--animation-duration) infinite}
.nc-loop-dots-4-24-icon-o :nth-child(1){transform-origin:4px 12px;animation-delay:-.3s;animation-delay:calc(var(--animation-duration)/-2.666)}
.nc-loop-dots-4-24-icon-o :nth-child(2){transform-origin:12px 12px;animation-delay:-.15s;animation-delay:calc(var(--animation-duration)/-5.333)}
.nc-loop-dots-4-24-icon-o :nth-child(3){transform-origin:20px 12px}
@keyframes nc-loop-dots-4-anim{0%,100%{opacity:.4;transform:scale(.75)}50%{opacity:1;transform:scale(1)}}
</style>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.25 6.875H3.75C3.40482 6.875 3.125 7.15482 3.125 7.5V16.25C3.125 16.5952 3.40482 16.875 3.75 16.875H16.25C16.5952 16.875 16.875 16.5952 16.875 16.25V7.5C16.875 7.15482 16.5952 6.875 16.25 6.875Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M7.1875 6.875V4.0625C7.1875 3.31658 7.48382 2.60121 8.01126 2.07376C8.53871 1.54632 9.25408 1.25 10 1.25C10.7459 1.25 11.4613 1.54632 11.9887 2.07376C12.5162 2.60121 12.8125 3.31658 12.8125 4.0625V6.875" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M10 13.125C10.6904 13.125 11.25 12.5654 11.25 11.875C11.25 11.1846 10.6904 10.625 10 10.625C9.30964 10.625 8.75 11.1846 8.75 11.875C8.75 12.5654 9.30964 13.125 10 13.125Z" fill="currentColor"></path>
</svg>

After

Width:  |  Height:  |  Size: 932 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" width="20" height="20"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 24 24" fill="currentColor"><g><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></g></svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@ -0,0 +1,41 @@
<div class="pswp" tabindex="-1" role="dialog" aria-hidden="true">
<div class="pswp__bg"></div>
<div class="pswp__scroll-wrap">
<div class="pswp__container">
<div class="pswp__item"></div>
<div class="pswp__item"></div>
<div class="pswp__item"></div>
</div>
<div class="pswp__ui pswp__ui--hidden">
<div class="pswp__top-bar">
<div class="pswp__counter"></div>
<button class="pswp__button pswp__button--close" title="Close (Esc)"></button>
<button class="pswp__button pswp__button--share" title="Share"></button>
<button class="pswp__button pswp__button--fs" title="Toggle fullscreen"></button>
<button class="pswp__button pswp__button--zoom" title="Zoom in/out"></button>
<div class="pswp__preloader">
<div class="pswp__preloader__icn">
<div class="pswp__preloader__cut">
<div class="pswp__preloader__donut"></div>
</div>
</div>
</div>
</div>
<div class="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div class="pswp__share-tooltip"></div>
</div>
<button class="pswp__button pswp__button--arrow--left" title="Previous (arrow left)"></button>
<button class="pswp__button pswp__button--arrow--right" title="Next (arrow right)"></button>
<div class="pswp__caption">
<div class="pswp__caption__center"></div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,35 @@
<article class="gh-card {{post_class}}{{#unless @custom.show_images_in_feed}} no-image{{/unless}}">
<a class="gh-card-link" href="{{url}}">
{{#if feature_image}}
<figure class="gh-card-image">
<img
srcset="{{img_url feature_image size="s"}} 300w,
{{img_url feature_image size="m"}} 720w,
{{img_url feature_image size="l"}} 960w,
{{img_url feature_image size="xl"}} 1200w,
{{img_url feature_image size="xxl"}} 2000w"
sizes="(max-width: 1200px) 100vw, 1200px"
src="{{img_url feature_image size="m"}}"
alt="{{#if feature_image_alt}}{{feature_image_alt}}{{else}}{{title}}{{/if}}"
>
</figure>
{{/if}}
<div class="gh-card-wrapper">
{{#if primary_tag}}
<p class="gh-card-tag">{{primary_tag.name}}</p>
{{/if}}
<h3 class="gh-card-title is-title">{{title}}</h3>
{{#if excerpt}}
<p class="gh-card-excerpt is-body">{{excerpt}}</p>
{{/if}}
<footer class="gh-card-meta"><!--
-->{{#if @custom.show_author}}
<span class="gh-card-author">By {{#foreach authors}}{{#if @first}}{{name}}{{else}}, {{name}}{{/if}}{{/foreach}}</span>
{{/if}}
{{#if @custom.show_publish_date}}
<time class="gh-card-date" datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
{{/if}}<!--
--></footer>
</div>
</a>
</article>

View File

@ -0,0 +1,3 @@
<button class="gh-search gh-icon-button" aria-label="Search this site" data-ghost-search>
{{> "icons/search"}}
</button>

View File

@ -0,0 +1,63 @@
{{!< default}}
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
{{#post}}
<main class="gh-main">
<article class="gh-article {{post_class}}">
<header class="gh-article-header gh-canvas">
{{#if primary_tag}}
<a class="gh-article-tag" href="{{primary_tag.url}}">{{primary_tag.name}}</a>
{{/if}}
<h1 class="gh-article-title is-title">{{title}}</h1>
{{#if custom_excerpt}}
<p class="gh-article-excerpt is-body">{{custom_excerpt}}</p>
{{/if}}
<div class="gh-article-meta">
<div class="gh-article-author-image">
{{#foreach authors}}
{{#if profile_image}}
<a href="{{url}}">
<img class="author-profile-image" src="{{img_url profile_image size="xs"}}" alt="{{name}}" />
</a>
{{else}}
<a href="{{url}}">{{> "icons/avatar"}}</a>
{{/if}}
{{/foreach}}
</div>
<div class="gh-article-meta-wrapper">
<h4 class="gh-article-author-name">{{authors}}</h4>
<div class="gh-article-meta-content">
<time class="gh-article-meta-date" datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
{{#if reading_time}}
<span class="gh-article-meta-length"><span class="bull">—</span> {{reading_time}}</span>
{{/if}}
</div>
</div>
</div>
{{> "feature-image"}}
</header>
<section class="gh-content gh-canvas is-body">
{{content}}
</section>
</article>
{{#if comments}}
<div class="gh-comments gh-canvas">
{{comments}}
</div>
{{/if}}
</main>
{{/post}}
{{> "components/post-list" feed="recent" postFeedStyle="Grid" title="Read more" showTitle=true showSidebar=false}}

View File

@ -0,0 +1,22 @@
{{!< default}}
{{!-- The tag above means: insert everything in this file into the body of the default.hbs template --}}
<main class="gh-main gh-outer">
{{#tag}}
<section class="gh-archive{{#if feature_image}} has-image{{/if}}{{#if @custom.show_site_in_sidebar}} has-sidebar{{/if}} gh-inner">
<div class="gh-archive-inner">
<header class="gh-archive-wrapper">
<h1 class="gh-article-title is-title">{{name}}</h1>
{{#if description}}
<p class="gh-article-excerpt">{{description}}</p>
{{/if}}
</header>
{{> "feature-image"}}
</div>
</section>
{{/tag}}
{{> "components/post-list" feed="archive" postFeedStyle=@custom.post_feed_style showTitle=false showSidebar=@custom.show_site_in_sidebar}}
</main>