├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .ghost.json ├── .github ├── ISSUE_TEMPLATE │ ├── ---bug-report.md │ └── --anything-else.md └── tokyo@2x.jpg ├── .gitignore ├── .nvmrc ├── LICENSE ├── Makefile ├── README.md ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── netlify.toml ├── package.json ├── plugins └── gatsby-plugin-ghost-manifest │ ├── .babelrc │ ├── common.js │ ├── gatsby-node.js │ ├── gatsby-ssr.js │ ├── index.js │ ├── package.json │ └── src │ ├── common.js │ ├── gatsby-node.js │ └── gatsby-ssr.js ├── renovate.json ├── src ├── components │ ├── common │ │ ├── Footer.js │ │ ├── Hamburger.js │ │ ├── Layout.js │ │ ├── Links.js │ │ ├── Navigation.js │ │ ├── NavigationLinks.js │ │ ├── Pagination.js │ │ ├── PostCard.js │ │ ├── index.js │ │ ├── meta │ │ │ ├── ArticleMeta.js │ │ │ ├── AuthorMeta.js │ │ │ ├── ImageMeta.js │ │ │ ├── MetaData.js │ │ │ ├── WebsiteMeta.js │ │ │ ├── getAuthorProperties.js │ │ │ └── index.js │ │ ├── navigation │ │ │ ├── Hamburger.js │ │ │ ├── Navigation.js │ │ │ ├── NavigationLinks.js │ │ │ └── index.js │ │ └── posts │ │ │ ├── PostAuthor.js │ │ │ ├── RecentPosts.js │ │ │ └── index.js │ └── sidebar │ │ ├── AuthorWidget.js │ │ ├── Sidebar.js │ │ ├── TwitterWidget.js │ │ └── index.js ├── images │ └── ghost-icon.png ├── pages │ └── 404.js ├── styles │ ├── app.less │ ├── author.less │ ├── content.less │ ├── footer.less │ ├── global.less │ ├── hamburger.less │ ├── index.less │ ├── layout.less │ ├── mixins.less │ ├── navigation.less │ ├── page.less │ ├── pagination.less │ ├── post │ │ ├── index.less │ │ ├── kg.less │ │ ├── post.less │ │ └── relatedposts.less │ ├── sidebar.less │ ├── tag.less │ └── variables.less ├── templates │ ├── author.js │ ├── index.js │ ├── page.js │ ├── post.js │ └── tag.js └── utils │ ├── fragments.js │ ├── rss │ └── generate-feed.js │ └── siteConfig.js └── static ├── css └── fonts.css ├── favicon.ico ├── favicon.png ├── fonts ├── AvenirNextLTPro-Medium.woff ├── AvenirNextLTPro-Medium.woff2 ├── AvenirNextLTPro-Regular.woff ├── AvenirNextLTPro-Regular.woff2 ├── FFMarkWebProBook.woff ├── FFMarkWebProBook.woff2 ├── FFMarkWebProMedium.woff └── FFMarkWebProMedium.woff2 ├── images ├── counter.svg ├── cover.jpg ├── icons │ ├── avatar.svg │ ├── facebook.svg │ ├── rss.svg │ └── twitter.svg ├── logo.svg └── logo@2x.png └── robots.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.hbs] 14 | insert_final_newline = false 15 | 16 | [*.json] 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.{yml,yaml}] 23 | indent_size = 2 24 | 25 | [Makefile] 26 | indent_style = tab 27 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/** 2 | plugins/**/*.js 3 | !plugins/*/src/*.js 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: `babel-eslint`, 3 | parserOptions: { 4 | ecmaVersion: 6, 5 | ecmaFeatures: { 6 | jsx: true, 7 | experimentalObjectRestSpread: true, 8 | }, 9 | }, 10 | plugins: [`ghost`, `react`, `node`, `promise`], 11 | extends: [ 12 | `plugin:ghost/node`, 13 | `plugin:ghost/ember`, 14 | `plugin:react/recommended`, 15 | `plugin:promise/recommended`, 16 | ], 17 | settings: { 18 | react: { 19 | createClass: `createReactClass`, 20 | pragma: `React`, 21 | version: `16.13.1`, 22 | flowVersion: `0.53`, 23 | }, 24 | propWrapperFunctions: [`forbidExtraProps`], 25 | }, 26 | env: { 27 | node: true, 28 | }, 29 | rules: { 30 | "ghost/sort-imports-es6-autofix/sort-imports-es6": `off`, 31 | "ghost/ember/use-ember-get-and-set": `off`, 32 | "no-console": `off`, 33 | indent: [`error`, 2], 34 | "no-inner-declarations": `off`, 35 | "valid-jsdoc": `off`, 36 | "require-jsdoc": `off`, 37 | quotes: [`error`, `backtick`], 38 | "consistent-return": [`error`], 39 | "arrow-body-style": [ 40 | `error`, 41 | `as-needed`, 42 | { requireReturnForObjectLiteral: true }, 43 | ], 44 | "jsx-quotes": [`error`, `prefer-double`], 45 | semi: [`error`, `never`], 46 | "object-curly-spacing": [`error`, `always`], 47 | "comma-dangle": [ 48 | `error`, 49 | { 50 | arrays: `always-multiline`, 51 | objects: `always-multiline`, 52 | imports: `always-multiline`, 53 | exports: `always-multiline`, 54 | functions: `ignore`, 55 | }, 56 | ], 57 | "react/prop-types": [ 58 | `error`, 59 | { 60 | ignore: [`children`], 61 | }, 62 | ], 63 | }, 64 | } 65 | -------------------------------------------------------------------------------- /.ghost.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "apiUrl": "https://toddbirchard.app", 4 | "contentApiKey": "95859e68b52aed4118f3b29d05" 5 | }, 6 | "production": { 7 | "apiUrl": "https://toddbirchard.app", 8 | "contentApiKey": "95859e68b52aed4118f3b29d05" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Report reproducible software issues so we can improve 4 | 5 | --- 6 | 7 | Welcome to the Gatsby Starter Ghost GitHub repo! 👋🎉 8 | 9 | We use GitHub only for bug reports 🐛 10 | 11 | Anything else should be posted to https://forum.ghost.org 👫 12 | 13 | For questions related to the usage of Gatsby or GraphQL, please check out their docs at https://www.gatsbyjs.org/ and https://graphql.org/ 14 | 15 | 🚨For support, help & questions use https://forum.ghost.org/c/help 16 | 💡For feature requests & ideas you can post and vote on https://forum.ghost.org/c/Ideas 17 | 18 | If your issue is with Gatsby.js itself, please report it at the Gatsby repo ➡️ https://github.com/gatsbyjs/gatsby/issues/new. 19 | 20 | ### Issue Summary 21 | 22 | A summary of the issue and the browser/OS environment in which it occurs. 23 | 24 | ### To Reproduce 25 | 26 | 1. This is the first step 27 | 2. This is the second step, etc. 28 | 29 | Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead? 30 | 31 | ### Technical details: 32 | 33 | * Ghost Version: 34 | * Gatsby Version: 35 | * Node Version: 36 | * OS: 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--anything-else.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1Anything else" 3 | about: "For help, support, features & ideas - please use https://forum.ghost.org \U0001F46B " 4 | 5 | --- 6 | 7 | --------------^ Click "Preview" for a nicer view! 8 | 9 | We use GitHub only for bug reports 🐛 10 | 11 | Anything else should be posted to https://forum.ghost.org 👫. 12 | 13 | 🚨For support, help & questions use https://forum.ghost.org/c/help 14 | 💡For feature requests & ideas you can post and vote on https://forum.ghost.org/c/Ideas 15 | 16 | Alternatively, check out these resources below. Thanks! 😁. 17 | 18 | - [Forum](https://forum.ghost.org/c/help) 19 | - [Gatsby API reference](https://docs.ghost.org/api/gatsby/) 20 | - [Content API Docs](https://docs.ghost.org/api/content/) 21 | - [Gatsby.js](https://www.gatsbyjs.org) 22 | - [GraphQL](https://graphql.org/) 23 | - [Feature Requests / Ideas](https://forum.ghost.org/c/Ideas) 24 | - [Contributing Guide](https://docs.ghost.org/docs/contributing) 25 | - [Self-hoster Docs](https://docs.ghost.org/) 26 | -------------------------------------------------------------------------------- /.github/tokyo@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/.github/tokyo@2x.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node template 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # IDE 63 | .idea/* 64 | *.iml 65 | *.sublime-* 66 | 67 | # OSX 68 | .DS_Store 69 | .vscode 70 | 71 | # Docs Custom 72 | .cache/ 73 | public 74 | yarn-error.log 75 | .netlify/ 76 | 77 | 78 | .env.* 79 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2019 Ghost Foundation 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRCPATH := $(CURDIR) 2 | 3 | define HELP 4 | This is the Tokyo project Makefile. 5 | 6 | Usage: 7 | 8 | make build - Build site & Lambdas for production. 9 | make serve - Build & serve production build locally. 10 | make clean - Purge cache, modules, lock files. 11 | make reset - Purge cache & reinstall modules. 12 | make update - Update npm production dependencies. 13 | make functions - Build Golang functions locally. 14 | endef 15 | export HELP 16 | 17 | .PHONY: build serve clean reset update help 18 | 19 | all help: 20 | @echo "$$HELP" 21 | 22 | build: 23 | npm run-script build 24 | 25 | .PHONY: serve 26 | serve: 27 | gatsby clean 28 | gatsby build 29 | gatsby serve 30 | 31 | .PHONY: clean 32 | clean: 33 | gatsby clean 34 | find . -name 'package-lock.json' -delete 35 | find . -name 'yarn.lock' -delete 36 | find . -wholename '.yarn' -delete 37 | find . -wholename '**/node_modules' -delete 38 | 39 | .PHONY: reset 40 | reset: clean 41 | npm i 42 | npm audit fix 43 | 44 | .PHONY: update 45 | update: 46 | ncu -u --dep=prod 47 | make clean && yarn install 48 | 49 | .PHONY: functions 50 | functions: 51 | mkdir -p functions 52 | GOOS=linux 53 | GOARCH=amd64 54 | GOBIN=${PWD}/functions-src/scrape go install ./... 55 | # go build -o functions ./... 56 | 57 | .PHONY: buildbackup 58 | buildbackup: 59 | npm run-script build 60 | mkdir -p functions 61 | GOOS=linux 62 | GOARCH=amd64 63 | GOBIN=${PWD}/functions go install ./... 64 | GOBIN=${PWD}/functions go build -o functions/scrape ./... 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tokyo: GatsbyJS Ghost Theme 2 | 3 | [![Netlify Status](https://api.netlify.com/api/v1/badges/efe12261-c4b4-498c-b74a-ba438133c252/deploy-status)](https://app.netlify.com/sites/sleepy-shirley-d61a1e/deploys) 4 | ![Ghost](https://img.shields.io/badge/Ghost-^v3.0.0-lightgrey.svg?longCache=true&style=flat-square&logo=ghost&logoColor=white&colorB=656c82&colorA=4c566a) 5 | ![Node](https://img.shields.io/badge/NodeJS-^v12-green.svg?longCache=true&style=flat-square&logo=node.js&logoColor=white&colorB=a3be8c&colorA=4c566a) 6 | ![Gatsby](https://img.shields.io/badge/Gatsby-v^2.8-yellow.svg?longCache=true&style=flat-square&logo=Gatsby&logoColor=white&colorA=4c566a&colorB=b48ead) 7 | ![Less](https://img.shields.io/badge/Less-v^3.10.3-blue.svg?longCache=true&logo=javascript&longCache=true&style=flat-square&logoColor=white&colorB=5e81ac&colorA=4c566a) 8 | ![GitHub Last Commit](https://img.shields.io/github/last-commit/google/skia.svg?style=flat-square&colorA=4c566a&colorB=a3be8c&logo=GitHub) 9 | [![GitHub issues](https://img.shields.io/github/issues/toddbirchard/gatsby-ghost-tokyo.svg?style=flat-square&colorB=ebcb8b&colorA=4c566a&logo=GitHub)](https://github.com/toddbirchard/gatsby-ghost-tokyo/issues) 10 | [![GitHub stars](https://img.shields.io/github/stars/toddbirchard/gatsby-ghost-tokyo.svg?style=flat-square&colorB=ebcb8b&colorA=4c566a&logo=GitHub)](https://github.com/toddbirchard/gatsby-ghost-tokyo/stargazers) 11 | [![GitHub forks](https://img.shields.io/github/forks/toddbirchard/gatsby-ghost-tokyo.svg?style=flat-square&colorA=4c566a&colorB=ebcb8b&logo=GitHub)](https://github.com/toddbirchard/gatsby-ghost-tokyo/network) 12 | 13 | Ghost theme suitable for creators focused on quality content. Lightweight yet tasteful collection of features intended to elevate authors. Live preview can be seen here: https://toddbirchard.com 14 | 15 | ![Tokyo Theme](./.github/tokyo@2x.jpg) 16 | 17 | ## About 18 | 19 | **Tokyo** is a minimalist Ghost theme emphasizing readability, load times, and customization. Stays true to a philosophy of simplicity while expanding on Ghost features to elevate authors. 20 | 21 | ### Features 22 | - Responsive layout 23 | - Featured hero pages 24 | - Related articles widget 25 | - Twitter widget 26 | - Tag cloud widget 27 | - Author widget 28 | 29 | ## Getting Started 30 | 31 | ### Installation 32 | 33 | The easiest way to install is by using [gatsby-cli](https://www.npmjs.com/package/gatsby-cli): 34 | 35 | ```bash 36 | $ gatsby new gatsby-starter-ghost https://github.com/toddbirchard/gatsby-ghost-tokyo.git 37 | ``` 38 | 39 | Otherwise the repo can be initiated as any other Gatsby app: 40 | 41 | ```bash 42 | $ git clone https://github.com/toddbirchard/gatsby-ghost-tokyo.git 43 | $ cd gatsby-starter-ghost 44 | $ npm install 45 | $ gatsby develop 46 | ``` 47 | 48 | This theme will source from toddbirchard.com by default; you'll need to edit the `.ghost.json` config file with your own credentials to source content. Create an "integration" in your Ghost CMS, and change the `apiUrl` and `contentApiKey` values of `.ghost.json` to match those generated by your integration. 49 | 50 | **Templates:** 51 | - `index.js` - Home page 52 | - `post.js` - Individual posts 53 | - `page.js` - Standalone pages 54 | - `tag.js` - Tag archives 55 | - `author.js` - Author archives 56 | 57 | **Widgets** 58 | - Twitter timeline 59 | - Tags 60 | - Social 61 | - Related posts 62 | 63 | ## Roadmap 64 | 65 | This theme is still in active development. 66 | 67 | ### Upcoming changes 68 | 69 | - Github profile widget 70 | - LESS refactor 71 | - Additional widget options 72 | - Speed optimizations 73 | - Documentation 74 | 75 | ----- 76 | 77 | This theme and all publically-visible repositories are free of charge. If you find this project to be helpful, a [small donation](https://www.buymeacoffee.com/hackersslackers) would be greatly appreciated to keep us in business. All proceeds go towards coffee, and all coffee goes towards improving these projects. 78 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | exports.onRouteUpdate = function () { 3 | trustAllScripts(); 4 | }; 5 | */ 6 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`) 2 | const config = require(`./src/utils/siteConfig`) 3 | const generateRSSFeed = require(`./src/utils/rss/generate-feed`) 4 | require(`dotenv`).config({ 5 | path: `.env.${process.env.NODE_ENV}` 6 | }) 7 | 8 | let ghostConfig 9 | 10 | try { 11 | ghostConfig = require(`./.ghost`) 12 | } catch (e) { 13 | ghostConfig = { 14 | production: { 15 | apiUrl: process.env.GHOST_API_URL, 16 | contentApiKey: process.env.GHOST_CONTENT_API_KEY, 17 | }, 18 | development: { 19 | apiUrl: process.env.GHOST_API_URL, 20 | contentApiKey: process.env.GHOST_CONTENT_API_KEY, 21 | }, 22 | } 23 | } finally { 24 | const { 25 | apiUrl, 26 | contentApiKey, 27 | } = process.env.NODE_ENV === `development` ? ghostConfig.development : ghostConfig.production 28 | 29 | if (!apiUrl || !contentApiKey || contentApiKey.match(//)) { 30 | throw new Error(`GHOST_API_URL and GHOST_CONTENT_API_KEY are required to build. Check the README.`) // eslint-disable-line 31 | } 32 | } 33 | 34 | module.exports = { 35 | siteMetadata: { 36 | title: config.shortTitle, 37 | siteUrl: config.siteUrl, 38 | description: config.siteDescriptionMeta, 39 | url: config.siteUrl, 40 | image: config.images.siteIcon, 41 | twitterUsername: config.links.twitter, 42 | flags: { 43 | // PRESERVE_WEBPACK_CACHE: true, 44 | FAST_DEV: true, 45 | FAST_REFRESH: true, 46 | PARALLEL_SOURCING: true, 47 | }, 48 | plugins: [ 49 | /** 50 | * Source Plugins 51 | */ 52 | { 53 | resolve: `gatsby-source-filesystem`, 54 | options: { 55 | path: path.join(__dirname, `src`, `pages`), 56 | name: `pages`, 57 | }, 58 | }, 59 | { 60 | resolve: `gatsby-source-filesystem`, 61 | options: { 62 | path: path.join(__dirname, `src`, `images`), 63 | name: `images`, 64 | }, 65 | }, 66 | { 67 | resolve: `gatsby-source-twitter`, 68 | options: { 69 | credentials: { 70 | consumer_key: process.env.TWITTER_CONSUMER_KEY, 71 | consumer_secret: process.env.TWITTER_CONSUMER_SECRET, 72 | bearer_token: process.env.TWITTER_BEARER_TOKEN, 73 | }, 74 | queries: { 75 | SiteTweets: { 76 | endpoint: `statuses/user_timeline`, 77 | params: { 78 | screen_name: `toddrbirchard`, 79 | include_rts: true, 80 | exclude_replies: true, 81 | tweet_mode: `extended`, 82 | count: 40, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | /** 89 | * Style Plugins 90 | */ 91 | { 92 | resolve: `gatsby-plugin-less`, 93 | options: { 94 | javascriptEnabled: true, 95 | }, 96 | }, 97 | `gatsby-plugin-sharp`, 98 | `gatsby-transformer-sharp`, 99 | { 100 | resolve: `gatsby-source-ghost`, 101 | options: process.env.NODE_ENV === `development` ? 102 | ghostConfig.development : ghostConfig.production, 103 | }, 104 | { 105 | resolve: `gatsby-plugin-web-font-loader`, 106 | options: { 107 | custom: { 108 | families: [ 109 | `AvenirNextLTPro-Regular`, 110 | `FFMarkWebProBook`, 111 | `AvenirNextLTPro-Medium`, 112 | `FFMarkWebProMedium` 113 | ], 114 | urls: [`/css/fonts.css`], 115 | }, 116 | timeout: 10000, 117 | }, 118 | }, 119 | { 120 | resolve: `gatsby-plugin-eslint`, 121 | options: { 122 | test: /\.js$|\.jsx$/, 123 | exclude: /(node_modules|.cache|public)/, 124 | stages: [`develop`], 125 | options: { 126 | emitWarning: true, 127 | failOnError: false, 128 | }, 129 | }, 130 | }, 131 | /** 132 | * SEO & Feed Plugins 133 | */ 134 | { 135 | resolve: `gatsby-plugin-ghost-manifest`, 136 | options: { 137 | short_name: config.shortTitle, 138 | start_url: `/`, 139 | background_color: config.backgroundColor, 140 | theme_color: config.themeColor, 141 | display: `minimal-ui`, 142 | icon: `static/${config.siteIcon}`, 143 | legacy: true, 144 | query: `{ 145 | allGhostSettings { 146 | edges { 147 | node { 148 | title 149 | description 150 | } 151 | } 152 | } 153 | }`, 154 | }, 155 | }, 156 | { 157 | resolve: `gatsby-plugin-feed`, 158 | options: { 159 | query: `{ 160 | allGhostSettings { 161 | edges { 162 | node { 163 | title 164 | description 165 | } 166 | } 167 | } 168 | }`, 169 | feeds: [ 170 | generateRSSFeed(config), 171 | ], 172 | }, 173 | }, 174 | { 175 | resolve: `gatsby-plugin-advanced-sitemap`, 176 | options: { 177 | query: `{ 178 | allGhostPost { 179 | edges { 180 | node { 181 | id 182 | slug 183 | updated_at 184 | created_at 185 | feature_image 186 | } 187 | } 188 | } 189 | allGhostPage { 190 | edges { 191 | node { 192 | id 193 | slug 194 | updated_at 195 | created_at 196 | feature_image 197 | } 198 | } 199 | } 200 | allGhostTag { 201 | edges { 202 | node { 203 | id 204 | slug 205 | feature_image 206 | } 207 | } 208 | } 209 | allGhostAuthor { 210 | edges { 211 | node { 212 | id 213 | slug 214 | profile_image 215 | } 216 | } 217 | } 218 | }`, 219 | mapping: { 220 | allGhostPost: { 221 | sitemap: `posts`, 222 | }, 223 | allGhostTag: { 224 | sitemap: `tags`, 225 | }, 226 | allGhostAuthor: { 227 | sitemap: `authors`, 228 | }, 229 | allGhostPage: { 230 | sitemap: `pages`, 231 | }, 232 | }, 233 | exclude: [ 234 | `/dev-404-page`, 235 | `/404`, 236 | `/404.html`, 237 | `/offline-plugin-app-shell-fallback`, 238 | ], 239 | createLinkInHead: true, 240 | addUncaughtPages: true, 241 | }, 242 | }, 243 | { 244 | resolve: `gatsby-plugin-robots-txt`, 245 | options: { 246 | host: config.siteUrl, 247 | sitemap: `${config.siteUrl}/sitemap.xml`, 248 | policy: [{ 249 | userAgent: `*`, 250 | allow: `/`, 251 | disallow: [`/ghost/`, `/p/`] 252 | }, ], 253 | output: `${config.siteUrl}/robots.txt`, 254 | }, 255 | }, 256 | { 257 | resolve: `gatsby-plugin-canonical-urls`, 258 | options: { 259 | siteUrl: config.siteUrl, 260 | stripQueryString: true, 261 | }, 262 | }, 263 | `gatsby-plugin-react-helmet`, 264 | `gatsby-plugin-offline`, 265 | /** 266 | * Misc Plugins 267 | */ 268 | `gatsby-plugin-force-trailing-slashes`, 269 | ], 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const path = require(`path`) 2 | const { postsPerPage } = require(`./src/utils/siteConfig`) 3 | const { paginate } = require(`gatsby-awesome-pagination`) 4 | 5 | /** 6 | * Here is the place where Gatsby creates the URLs for all the 7 | * posts, tags, pages and authors that we fetched from the Ghost site. 8 | */ 9 | exports.createPages = async ({ graphql, actions }) => { 10 | const { createPage } = actions 11 | 12 | const result = await graphql(` 13 | { 14 | allGhostPost(sort: { order: ASC, fields: published_at }) { 15 | edges { 16 | node { 17 | slug 18 | primary_tag { 19 | slug 20 | name 21 | } 22 | tags { 23 | slug 24 | name 25 | visibility 26 | } 27 | } 28 | } 29 | } 30 | allGhostTag(sort: { order: ASC, fields: name }) { 31 | edges { 32 | node { 33 | slug 34 | url 35 | name 36 | postCount 37 | } 38 | } 39 | } 40 | allGhostAuthor(sort: { order: ASC, fields: name }) { 41 | edges { 42 | node { 43 | slug 44 | url 45 | postCount 46 | } 47 | } 48 | } 49 | allGhostPage(sort: { order: ASC, fields: published_at }) { 50 | edges { 51 | node { 52 | slug 53 | url 54 | postCount 55 | twitter 56 | website 57 | } 58 | } 59 | } 60 | } 61 | `) 62 | 63 | // Check for any errors 64 | if (result.errors) { 65 | throw new Error(result.errors) 66 | } 67 | 68 | // Extract query results 69 | const tags = result.data.allGhostTag.edges 70 | const authors = result.data.allGhostAuthor.edges 71 | const pages = result.data.allGhostPage.edges 72 | const posts = result.data.allGhostPost.edges 73 | 74 | // Load templates 75 | const indexTemplate = path.resolve(`./src/templates/index.js`) 76 | const tagsTemplate = path.resolve(`./src/templates/tag.js`) 77 | const authorTemplate = path.resolve(`./src/templates/author.js`) 78 | const pageTemplate = path.resolve(`./src/templates/page.js`) 79 | const postTemplate = path.resolve(`./src/templates/post.js`) 80 | 81 | // Create tag pages 82 | tags.forEach(({ node }) => { 83 | const totalPosts = node.postCount !== null ? node.postCount : 0 84 | const numberOfPages = Math.ceil(totalPosts / postsPerPage) 85 | 86 | // This part here defines, that our tag pages will use 87 | // a `/tag/:slug/` permalink. 88 | node.url = `/tag/${node.slug}/` 89 | 90 | Array.from({ length: numberOfPages }).forEach((_, i) => { 91 | const currentPage = i + 1 92 | const prevPageNumber = currentPage <= 1 ? null : currentPage - 1 93 | const nextPageNumber = 94 | currentPage + 1 > numberOfPages ? null : currentPage + 1 95 | const previousPagePath = prevPageNumber 96 | ? prevPageNumber === 1 97 | ? node.url 98 | : `${node.url}page/${prevPageNumber}/` 99 | : null 100 | const nextPagePath = nextPageNumber 101 | ? `${node.url}page/${nextPageNumber}/` 102 | : null 103 | 104 | createPage({ 105 | path: i === 0 ? node.url : `${node.url}page/${i + 1}/`, 106 | component: tagsTemplate, 107 | context: { 108 | // Data passed to context is available 109 | // in page queries as GraphQL variables. 110 | slug: node.slug, 111 | limit: postsPerPage, 112 | skip: i * postsPerPage, 113 | numberOfPages: numberOfPages, 114 | humanPageNumber: currentPage, 115 | prevPageNumber: prevPageNumber, 116 | nextPageNumber: nextPageNumber, 117 | previousPagePath: previousPagePath, 118 | nextPagePath: nextPagePath, 119 | }, 120 | }) 121 | }) 122 | }) 123 | 124 | // Create author pages 125 | authors.forEach(({ node }) => { 126 | const totalPosts = node.postCount !== null ? node.postCount : 0 127 | const numberOfPages = Math.ceil(totalPosts / postsPerPage) 128 | 129 | // This part here defines, that our author pages will use 130 | // a `/author/:slug/` permalink. 131 | node.url = `/author/${node.slug}/` 132 | 133 | Array.from({ length: numberOfPages }).forEach((_, i) => { 134 | const currentPage = i + 1 135 | const prevPageNumber = currentPage <= 1 ? null : currentPage - 1 136 | const nextPageNumber = 137 | currentPage + 1 > numberOfPages ? null : currentPage + 1 138 | const previousPagePath = prevPageNumber 139 | ? prevPageNumber === 1 140 | ? node.url 141 | : `${node.url}page/${prevPageNumber}/` 142 | : null 143 | const nextPagePath = nextPageNumber 144 | ? `${node.url}page/${nextPageNumber}/` 145 | : null 146 | 147 | createPage({ 148 | path: i === 0 ? node.url : `${node.url}page/${i + 1}/`, 149 | component: authorTemplate, 150 | context: { 151 | // Data passed to context is available 152 | // in page queries as GraphQL variables. 153 | slug: node.slug, 154 | limit: postsPerPage, 155 | skip: i * postsPerPage, 156 | numberOfPages: numberOfPages, 157 | humanPageNumber: currentPage, 158 | prevPageNumber: prevPageNumber, 159 | nextPageNumber: nextPageNumber, 160 | previousPagePath: previousPagePath, 161 | nextPagePath: nextPagePath, 162 | }, 163 | }) 164 | }) 165 | }) 166 | 167 | // Create pages 168 | pages.forEach(({ node }) => { 169 | // This part here defines, that our pages will use 170 | // a `/:slug/` permalink. 171 | node.url = `/${node.slug}/` 172 | 173 | createPage({ 174 | path: node.url, 175 | component: pageTemplate, 176 | context: { 177 | // Data passed to context is available 178 | // in page queries as GraphQL variables. 179 | slug: node.slug, 180 | }, 181 | }) 182 | }) 183 | 184 | // Create post pages 185 | posts.forEach(({ node }) => { 186 | // This part here defines, that our posts will use 187 | // a `/:slug/` permalink. 188 | node.url = `/${node.slug}/` 189 | 190 | createPage({ 191 | path: node.url, 192 | component: postTemplate, 193 | context: { 194 | // Data passed to context is available 195 | // in page queries as GraphQL variables. 196 | slug: node.slug, 197 | }, 198 | }) 199 | }) 200 | 201 | // Create pagination 202 | paginate({ 203 | createPage, 204 | items: posts, 205 | itemsPerPage: postsPerPage, 206 | component: indexTemplate, 207 | pathPrefix: ({ pageNumber }) => { 208 | if (pageNumber === 0) { 209 | return `/` 210 | } else { 211 | return `/page` 212 | } 213 | }, 214 | }) 215 | } 216 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "gatsby build" 3 | publish = "public/" 4 | 5 | [template] 6 | incoming-hooks = ["Ghost"] 7 | 8 | [[headers]] 9 | for = "/fonts/*" 10 | [headers.values] 11 | crossorigin = "anonymous" 12 | type = "font/woff2" 13 | accept = "application/font-woff2" 14 | cache-control = ''' 15 | max-age=604800, 16 | no-cache, 17 | public''' 18 | 19 | [[headers]] 20 | for = "/images/counter.svg" 21 | [headers.values] 22 | crossorigin = "anonymous" 23 | type = "image/svg+xml" 24 | cache-control = ''' 25 | max-age=0, 26 | no-cache, 27 | no-store, 28 | must-revalidate''' 29 | 30 | [[headers]] 31 | for = "/rss.xml" 32 | [headers.values] 33 | content-type = "text/xml; charset=utf-8" 34 | 35 | [[headers]] 36 | for = "*" 37 | [headers.values] 38 | Access-Control-Allow-Origin = "*" 39 | 40 | [build.processing] 41 | skip_processing = false 42 | [build.processing.css] 43 | bundle = true 44 | minify = true 45 | [build.processing.js] 46 | bundle = true 47 | minify = true 48 | [build.processing.html] 49 | pretty_urls = true 50 | [build.processing.images] 51 | compress = true 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tokyo", 3 | "description": "GatsbyJS theme suitable for authors focused on quality content. Lightweight yet tasteful collection of features intended to elevate authors.", 4 | "version": "1.0.0", 5 | "homepage": "https://github.com/toddbirchard/gatsby-ghost-tokyo/", 6 | "author": { 7 | "name": "Todd Birchard", 8 | "email": "toddbirchard@gmail.com", 9 | "url": "https://toddbirchard.com" 10 | }, 11 | "keywords": [ 12 | "gatsby", 13 | "ghost", 14 | "blog", 15 | "theme", 16 | "JAMStack" 17 | ], 18 | "engines": { 19 | "node": ">= 14" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/toddbirchard/gatsby-ghost-tokyo.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/toddbirchard/gatsby-ghost-tokyo/issues" 27 | }, 28 | "main": "n/a", 29 | "scripts": { 30 | "serve": "gatsby build && NODE_ENV=production gatsby serve", 31 | "build": "gatsby build", 32 | "dev": "gatsby develop", 33 | "lint": "eslint . --ext .js --cache", 34 | "test": "echo \"Error: no test specified\" && exit 1" 35 | }, 36 | "devDependencies": { 37 | "@babel/eslint-parser": "7.14.4", 38 | "autoprefixer": "^10.2.6", 39 | "babel-preset-gatsby": "2.2.0", 40 | "babel-preset-gatsby": "^1.6.0", 41 | "eslint": "8.3.0", 42 | "eslint-loader": "4.0.2", 43 | "eslint-plugin-ghost": "2.7.0", 44 | "eslint-plugin-node": "11.1.0", 45 | "eslint-plugin-promise": "5.1.1", 46 | "eslint-plugin-react": "7.24.0", 47 | "less": "4.1.2", 48 | "qs": "6.10.1", 49 | "stylelint": "13.13.1", 50 | "stylelint-config-standard": "24.0.0" 51 | }, 52 | "dependencies": { 53 | "@tryghost/helpers": "^1.1.45", 54 | "@tryghost/helpers-gatsby": "^1.0.50", 55 | "gatsby": "4.2.0", 56 | "cheerio": "1.0.0-rc.10", 57 | "gatsby-awesome-pagination": "0.3.8", 58 | "gatsby-plugin-advanced-sitemap": "^2.0.0", 59 | "gatsby-plugin-feed": "4.2.0", 60 | "gatsby-plugin-force-trailing-slashes": "1.0.5", 61 | "gatsby-plugin-image": "2.2.0", 62 | "gatsby-plugin-less": "^6.0.0", 63 | "gatsby-plugin-manifest": "4.2.0", 64 | "gatsby-plugin-offline": "5.2.0", 65 | "gatsby-plugin-preload-fonts": "^3.0.0", 66 | "gatsby-plugin-react-helmet": "5.2.0", 67 | "gatsby-plugin-sharp": "4.2.0", 68 | "gatsby-plugin-web-font-loader": "^1.0.4", 69 | "gatsby-source-filesystem": "4.2.0", 70 | "gatsby-source-ghost": "4.2.4", 71 | "gatsby-source-twitter": "^4.0.0", 72 | "gatsby-transformer-sharp": "^4.0.0", 73 | "lodash": "4.17.21", 74 | "react": "^17.0.0", 75 | "react-burger-menu": "^3.0.6", 76 | "react-dom": "^17.0.2", 77 | "react-hamburger-menu": "^1.2.1", 78 | "react-helmet": "6.1.0", 79 | "react-icons": "^4.2.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "babel-preset-gatsby-package", 5 | { 6 | "browser": true 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/common.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require("fs"); // default icons for generating icons 4 | 5 | 6 | exports.defaultIcons = [{ 7 | src: "icons/icon-48x48.png", 8 | sizes: "48x48", 9 | type: "image/png" 10 | }, { 11 | src: "icons/icon-72x72.png", 12 | sizes: "72x72", 13 | type: "image/png" 14 | }, { 15 | src: "icons/icon-96x96.png", 16 | sizes: "96x96", 17 | type: "image/png" 18 | }, { 19 | src: "icons/icon-144x144.png", 20 | sizes: "144x144", 21 | type: "image/png" 22 | }, { 23 | src: "icons/icon-192x192.png", 24 | sizes: "192x192", 25 | type: "image/png" 26 | }, { 27 | src: "icons/icon-256x256.png", 28 | sizes: "256x256", 29 | type: "image/png" 30 | }, { 31 | src: "icons/icon-384x384.png", 32 | sizes: "384x384", 33 | type: "image/png" 34 | }, { 35 | src: "icons/icon-512x512.png", 36 | sizes: "512x512", 37 | type: "image/png" 38 | }]; 39 | /** 40 | * Check if the icon exists on the filesystem 41 | * 42 | * @param {String} srcIcon Path of the icon 43 | */ 44 | 45 | exports.doesIconExist = function doesIconExist(srcIcon) { 46 | try { 47 | return fs.statSync(srcIcon).isFile(); 48 | } catch (e) { 49 | if (e.code === "ENOENT") { 50 | return false; 51 | } else { 52 | throw e; 53 | } 54 | } 55 | }; -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/gatsby-node.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); 6 | 7 | var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); 8 | 9 | var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose")); 10 | 11 | var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator")); 12 | 13 | var fs = require("fs"); 14 | 15 | var path = require("path"); 16 | 17 | var Promise = require("bluebird"); 18 | 19 | var sharp = require("sharp"); 20 | 21 | var _require = require("./common.js"), 22 | defaultIcons = _require.defaultIcons, 23 | doesIconExist = _require.doesIconExist; 24 | 25 | sharp.simd(true); 26 | 27 | function generateIcons(icons, srcIcon) { 28 | return Promise.map(icons, function (icon) { 29 | var size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf("x"))); 30 | var imgPath = path.join("public", icon.src); 31 | return sharp(srcIcon).resize(size).toFile(imgPath).then(function () {}); 32 | }); 33 | } 34 | 35 | exports.onPostBuild = /*#__PURE__*/function () { 36 | var _ref2 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(_ref, pluginOptions) { 37 | var graphql, icon, manifest, _yield$graphql, data, siteTitle, iconPath; 38 | 39 | return _regenerator.default.wrap(function _callee$(_context) { 40 | while (1) { 41 | switch (_context.prev = _context.next) { 42 | case 0: 43 | graphql = _ref.graphql; 44 | icon = pluginOptions.icon, manifest = (0, _objectWithoutPropertiesLoose2.default)(pluginOptions, ["icon"]); 45 | _context.next = 4; 46 | return graphql(pluginOptions.query); 47 | 48 | case 4: 49 | _yield$graphql = _context.sent; 50 | data = _yield$graphql.data; 51 | siteTitle = data.allGhostSettings.edges[0].node.title || "No Title"; 52 | manifest = (0, _extends2.default)({}, manifest, { 53 | name: siteTitle 54 | }); // Delete options we won't pass to the manifest.webmanifest. 55 | 56 | delete manifest.plugins; 57 | delete manifest.legacy; 58 | delete manifest.theme_color_in_head; 59 | delete manifest.query; // If icons are not manually defined, use the default icon set. 60 | 61 | if (!manifest.icons) { 62 | manifest.icons = defaultIcons; 63 | } // Determine destination path for icons. 64 | 65 | 66 | iconPath = path.join("public", path.dirname(manifest.icons[0].src)); //create destination directory if it doesn't exist 67 | 68 | if (!fs.existsSync(iconPath)) { 69 | fs.mkdirSync(iconPath); 70 | } 71 | 72 | fs.writeFileSync(path.join("public", "manifest.webmanifest"), JSON.stringify(manifest)); // Only auto-generate icons if a src icon is defined. 73 | 74 | if (icon !== undefined) { 75 | // Check if the icon exists 76 | if (!doesIconExist(icon)) { 77 | Promise.reject("icon (" + icon + ") does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site."); 78 | } 79 | 80 | generateIcons(manifest.icons, icon).then(function () { 81 | //images have been generated 82 | console.log("done generating icons for manifest"); 83 | Promise.resolve(); 84 | }); 85 | } else { 86 | Promise.resolve(); 87 | } 88 | 89 | case 17: 90 | case "end": 91 | return _context.stop(); 92 | } 93 | } 94 | }, _callee); 95 | })); 96 | 97 | return function (_x, _x2) { 98 | return _ref2.apply(this, arguments); 99 | }; 100 | }(); -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 4 | 5 | var _react = _interopRequireDefault(require("react")); 6 | 7 | var _gatsby = require("gatsby"); 8 | 9 | var _common = require("./common.js"); 10 | 11 | exports.onRenderBody = function (_ref, pluginOptions) { 12 | var setHeadComponents = _ref.setHeadComponents; 13 | // We use this to build a final array to pass as the argument to setHeadComponents at the end of onRenderBody. 14 | var headComponents = []; 15 | var icons = pluginOptions.icons || _common.defaultIcons; // If icons were generated, also add a favicon link. 16 | 17 | if (pluginOptions.icon) { 18 | var favicon = icons && icons.length ? icons[0].src : null; 19 | 20 | if (favicon) { 21 | headComponents.push( /*#__PURE__*/_react.default.createElement("link", { 22 | key: "gatsby-plugin-manifest-icon-link", 23 | rel: "shortcut icon", 24 | href: (0, _gatsby.withPrefix)(favicon) 25 | })); 26 | } 27 | } // Add manifest link tag. 28 | 29 | 30 | headComponents.push( /*#__PURE__*/_react.default.createElement("link", { 31 | key: "gatsby-plugin-manifest-link", 32 | rel: "manifest", 33 | href: (0, _gatsby.withPrefix)("/manifest.webmanifest") 34 | })); // The user has an option to opt out of the theme_color meta tag being inserted into the head. 35 | 36 | if (pluginOptions.theme_color) { 37 | var insertMetaTag = Object.keys(pluginOptions).includes("theme_color_in_head") ? pluginOptions.theme_color_in_head : true; 38 | 39 | if (insertMetaTag) { 40 | headComponents.push( /*#__PURE__*/_react.default.createElement("meta", { 41 | key: "gatsby-plugin-manifest-meta", 42 | name: "theme-color", 43 | content: pluginOptions.theme_color 44 | })); 45 | } 46 | } 47 | 48 | if (pluginOptions.legacy) { 49 | var iconLinkTags = icons.map(function (icon) { 50 | return /*#__PURE__*/_react.default.createElement("link", { 51 | key: "gatsby-plugin-manifest-apple-touch-icon-" + icon.sizes, 52 | rel: "apple-touch-icon", 53 | sizes: icon.sizes, 54 | href: (0, _gatsby.withPrefix)("" + icon.src) 55 | }); 56 | }); 57 | headComponents = [].concat(headComponents, iconLinkTags); 58 | } 59 | 60 | setHeadComponents(headComponents); 61 | }; -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/index.js: -------------------------------------------------------------------------------- 1 | // noop -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-plugin-ghost-manifest", 3 | "description": "Gatsby plugin which adds a manifest.webmanifest to make sites progressive web apps", 4 | "version": "0.0.1", 5 | "author": "Ghost Foundation", 6 | "dependencies": { 7 | "@babel/runtime": "7.14.0", 8 | "bluebird": "3.7.2", 9 | "sharp": "0.28.2" 10 | }, 11 | "devDependencies": { 12 | "@babel/cli": "7.14.3", 13 | "@babel/core": "7.14.3", 14 | "babel-preset-gatsby-package": "2.2.0", 15 | "cross-env": "7.0.3" 16 | }, 17 | "keywords": [ 18 | "gatsby", 19 | "gatsby-plugin", 20 | "favicon", 21 | "icons", 22 | "manifest.webmanifest", 23 | "progressive-web-app", 24 | "pwa" 25 | ], 26 | "resolutions": { 27 | "sharp": "0.28.2" 28 | }, 29 | "license": "MIT", 30 | "main": "index.js", 31 | "peerDependencies": { 32 | "gatsby": ">=2.24.3" 33 | }, 34 | "scripts": { 35 | "build": "babel src --out-dir . --ignore **/__tests__", 36 | "prepare": "cross-env NODE_ENV=production npm run build", 37 | "watch": "babel -w src --out-dir . --ignore **/__tests__" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/src/common.js: -------------------------------------------------------------------------------- 1 | const fs = require(`fs`) 2 | 3 | // default icons for generating icons 4 | exports.defaultIcons = [ 5 | { 6 | src: `icons/icon-48x48.png`, 7 | sizes: `48x48`, 8 | type: `image/png`, 9 | }, 10 | { 11 | src: `icons/icon-72x72.png`, 12 | sizes: `72x72`, 13 | type: `image/png`, 14 | }, 15 | { 16 | src: `icons/icon-96x96.png`, 17 | sizes: `96x96`, 18 | type: `image/png`, 19 | }, 20 | { 21 | src: `icons/icon-144x144.png`, 22 | sizes: `144x144`, 23 | type: `image/png`, 24 | }, 25 | { 26 | src: `icons/icon-192x192.png`, 27 | sizes: `192x192`, 28 | type: `image/png`, 29 | }, 30 | { 31 | src: `icons/icon-256x256.png`, 32 | sizes: `256x256`, 33 | type: `image/png`, 34 | }, 35 | { 36 | src: `icons/icon-384x384.png`, 37 | sizes: `384x384`, 38 | type: `image/png`, 39 | }, 40 | { 41 | src: `icons/icon-512x512.png`, 42 | sizes: `512x512`, 43 | type: `image/png`, 44 | }, 45 | ] 46 | 47 | /** 48 | * Check if the icon exists on the filesystem 49 | * 50 | * @param {String} srcIcon Path of the icon 51 | */ 52 | exports.doesIconExist = function doesIconExist(srcIcon) { 53 | try { 54 | return fs.statSync(srcIcon).isFile() 55 | } catch (e) { 56 | if (e.code === `ENOENT`) { 57 | return false 58 | } else { 59 | throw e 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/src/gatsby-node.js: -------------------------------------------------------------------------------- 1 | const fs = require(`fs`) 2 | const path = require(`path`) 3 | const Promise = require(`bluebird`) 4 | const sharp = require(`sharp`) 5 | const { defaultIcons, doesIconExist } = require(`./common.js`) 6 | 7 | sharp.simd(true) 8 | 9 | function generateIcons(icons, srcIcon) { 10 | return Promise.map(icons, (icon) => { 11 | const size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`))) 12 | const imgPath = path.join(`public`, icon.src) 13 | 14 | return sharp(srcIcon) 15 | .resize(size) 16 | .toFile(imgPath) 17 | .then(() => { }) 18 | }) 19 | } 20 | 21 | exports.onPostBuild = async ({ graphql }, pluginOptions) => { 22 | let { icon, ...manifest } = pluginOptions 23 | 24 | const { data } = await graphql(pluginOptions.query) 25 | const siteTitle = data.allGhostSettings.edges[0].node.title || `No Title` 26 | manifest = { 27 | ...manifest, 28 | name: siteTitle, 29 | } 30 | 31 | // Delete options we won't pass to the manifest.webmanifest. 32 | delete manifest.plugins 33 | delete manifest.legacy 34 | delete manifest.theme_color_in_head 35 | delete manifest.query 36 | 37 | // If icons are not manually defined, use the default icon set. 38 | if (!manifest.icons) { 39 | manifest.icons = defaultIcons 40 | } 41 | 42 | // Determine destination path for icons. 43 | const iconPath = path.join(`public`, path.dirname(manifest.icons[0].src)) 44 | 45 | //create destination directory if it doesn't exist 46 | if (!fs.existsSync(iconPath)) { 47 | fs.mkdirSync(iconPath) 48 | } 49 | 50 | fs.writeFileSync( 51 | path.join(`public`, `manifest.webmanifest`), 52 | JSON.stringify(manifest) 53 | ) 54 | 55 | // Only auto-generate icons if a src icon is defined. 56 | if (icon !== undefined) { 57 | // Check if the icon exists 58 | if (!doesIconExist(icon)) { 59 | Promise.reject( 60 | `icon (${icon}) does not exist as defined in gatsby-config.js. Make sure the file exists relative to the root of the site.` 61 | ) 62 | } 63 | generateIcons(manifest.icons, icon).then(() => { 64 | //images have been generated 65 | console.log(`done generating icons for manifest`) 66 | Promise.resolve() 67 | }) 68 | } else { 69 | Promise.resolve() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/src/gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { withPrefix } from "gatsby" 3 | import { defaultIcons } from "./common.js" 4 | 5 | exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => { 6 | // We use this to build a final array to pass as the argument to setHeadComponents at the end of onRenderBody. 7 | let headComponents = [] 8 | 9 | const icons = pluginOptions.icons || defaultIcons 10 | 11 | // If icons were generated, also add a favicon link. 12 | if (pluginOptions.icon) { 13 | let favicon = icons && icons.length ? icons[0].src : null 14 | 15 | if (favicon) { 16 | headComponents.push( 17 | 22 | ) 23 | } 24 | } 25 | 26 | // Add manifest link tag. 27 | headComponents.push( 28 | 33 | ) 34 | // The user has an option to opt out of the theme_color meta tag being inserted into the head. 35 | if (pluginOptions.theme_color) { 36 | let insertMetaTag = Object.keys(pluginOptions).includes( 37 | `theme_color_in_head` 38 | ) 39 | ? pluginOptions.theme_color_in_head 40 | : true 41 | 42 | if (insertMetaTag) { 43 | headComponents.push( 44 | 49 | ) 50 | } 51 | } 52 | 53 | if (pluginOptions.legacy) { 54 | const iconLinkTags = icons.map(icon => ( 55 | 61 | )) 62 | 63 | headComponents = [...headComponents, ...iconLinkTags] 64 | } 65 | 66 | setHeadComponents(headComponents) 67 | } -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/components/common/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Footer = ({ title }) => ( 5 | <> 6 | {/* The footer at the very bottom of the screen */} 7 | 12 | 13 | ) 14 | 15 | Footer.propTypes = { 16 | title: PropTypes.string.isRequired, 17 | } 18 | 19 | export default Footer 20 | -------------------------------------------------------------------------------- /src/components/common/Hamburger.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { slide as Menu } from "react-burger-menu" 3 | import { Link } from 'gatsby' 4 | 5 | const Hamburger = ({ data, navClass }) => ( 6 | // Pass on our props 7 | 8 | {data.map((navItem, i) => { 9 | if (navItem.url.match(/^\s?http(s?)/gi)) { 10 | return {navItem.label} 11 | } else { 12 | return {navItem.label} 13 | } 14 | })} 15 | ) 16 | 17 | export default Hamburger 18 | -------------------------------------------------------------------------------- /src/components/common/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Helmet } from 'react-helmet' 4 | import { Link, StaticQuery, graphql } from 'gatsby' 5 | import { Navigation, Footer } from '.' 6 | import { Sidebar } from '../sidebar/' 7 | 8 | import '../../styles/app.less' 9 | 10 | /** 11 | * Main layout component 12 | * 13 | * The Layout component wraps around each page and template. 14 | * It also provides the header, footer as well as the main 15 | * styles, and meta data for each page. 16 | * 17 | */ 18 | 19 | const DefaultLayout = ({ data, children, bodyClass, isHome, template }) => { 20 | const site = data.allGhostSettings.edges[0].node 21 | 22 | return ( 23 | <> 24 | 25 | 26 | 27 | 28 | 29 |
30 | { isHome && 31 |
32 | 33 | {site.logo ? {site.title} :

{site.title}

} 34 | 35 |
} 36 | 37 |
38 | {/* All the main content gets inserted here, index.js, post.js */} 39 | { isHome ? : null} 40 | {children} 41 |
42 |
43 | {/* The footer at the very bottom of the screen */} 44 |