├── .nvmrc ├── src ├── styles │ ├── mixins.less │ ├── page.less │ ├── post │ │ ├── index.less │ │ ├── relatedposts.less │ │ ├── post.less │ │ └── kg.less │ ├── footer.less │ ├── app.less │ ├── variables.less │ ├── tag.less │ ├── pagination.less │ ├── layout.less │ ├── hamburger.less │ ├── author.less │ ├── index.less │ ├── content.less │ ├── navigation.less │ ├── sidebar.less │ └── global.less ├── components │ ├── common │ │ ├── meta │ │ │ ├── index.js │ │ │ ├── ImageMeta.js │ │ │ ├── getAuthorProperties.js │ │ │ ├── MetaData.js │ │ │ ├── AuthorMeta.js │ │ │ ├── WebsiteMeta.js │ │ │ └── ArticleMeta.js │ │ ├── posts │ │ │ ├── index.js │ │ │ ├── RecentPosts.js │ │ │ └── PostAuthor.js │ │ ├── navigation │ │ │ ├── index.js │ │ │ ├── Hamburger.js │ │ │ ├── NavigationLinks.js │ │ │ └── Navigation.js │ │ ├── index.js │ │ ├── Footer.js │ │ ├── Hamburger.js │ │ ├── Pagination.js │ │ ├── Links.js │ │ ├── NavigationLinks.js │ │ ├── Navigation.js │ │ ├── PostCard.js │ │ └── Layout.js │ └── sidebar │ │ ├── index.js │ │ ├── AuthorWidget.js │ │ ├── Sidebar.js │ │ └── TwitterWidget.js ├── images │ └── ghost-icon.png ├── pages │ └── 404.js ├── utils │ ├── siteConfig.js │ ├── rss │ │ └── generate-feed.js │ └── fragments.js └── templates │ ├── index.js │ ├── page.js │ ├── tag.js │ ├── author.js │ └── post.js ├── static ├── robots.txt ├── favicon.ico ├── favicon.png ├── images │ ├── cover.jpg │ ├── logo@2x.png │ ├── counter.svg │ ├── icons │ │ ├── facebook.svg │ │ ├── avatar.svg │ │ ├── rss.svg │ │ └── twitter.svg │ └── logo.svg ├── fonts │ ├── FFMarkWebProBook.woff │ ├── FFMarkWebProBook.woff2 │ ├── FFMarkWebProMedium.woff │ ├── FFMarkWebProMedium.woff2 │ ├── AvenirNextLTPro-Medium.woff │ ├── AvenirNextLTPro-Medium.woff2 │ ├── AvenirNextLTPro-Regular.woff │ └── AvenirNextLTPro-Regular.woff2 └── css │ └── fonts.css ├── plugins └── gatsby-plugin-ghost-manifest │ ├── index.js │ ├── .babelrc │ ├── package.json │ ├── common.js │ ├── src │ ├── common.js │ ├── gatsby-ssr.js │ └── gatsby-node.js │ ├── gatsby-ssr.js │ └── gatsby-node.js ├── .eslintignore ├── renovate.json ├── .github ├── tokyo@2x.jpg └── ISSUE_TEMPLATE │ ├── --anything-else.md │ └── ---bug-report.md ├── gatsby-browser.js ├── .ghost.json ├── .editorconfig ├── LICENSE ├── netlify.toml ├── .gitignore ├── Makefile ├── .eslintrc.js ├── package.json ├── README.md ├── gatsby-node.js └── gatsby-config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /src/styles/mixins.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/index.js: -------------------------------------------------------------------------------- 1 | // noop -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/** 2 | plugins/**/*.js 3 | !plugins/*/src/*.js 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/components/common/meta/index.js: -------------------------------------------------------------------------------- 1 | export { default as MetaData } from './MetaData' 2 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/favicon.png -------------------------------------------------------------------------------- /.github/tokyo@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/.github/tokyo@2x.jpg -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /* 2 | exports.onRouteUpdate = function () { 3 | trustAllScripts(); 4 | }; 5 | */ 6 | -------------------------------------------------------------------------------- /static/images/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/images/cover.jpg -------------------------------------------------------------------------------- /src/images/ghost-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/src/images/ghost-icon.png -------------------------------------------------------------------------------- /static/images/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/images/logo@2x.png -------------------------------------------------------------------------------- /src/styles/page.less: -------------------------------------------------------------------------------- 1 | .page-template .content-title { 2 | padding-bottom: 20px; 3 | border-bottom: 1px solid #d8d8d8; 4 | } 5 | -------------------------------------------------------------------------------- /static/fonts/FFMarkWebProBook.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/FFMarkWebProBook.woff -------------------------------------------------------------------------------- /static/fonts/FFMarkWebProBook.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/FFMarkWebProBook.woff2 -------------------------------------------------------------------------------- /static/fonts/FFMarkWebProMedium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/FFMarkWebProMedium.woff -------------------------------------------------------------------------------- /static/fonts/FFMarkWebProMedium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/FFMarkWebProMedium.woff2 -------------------------------------------------------------------------------- /static/fonts/AvenirNextLTPro-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/AvenirNextLTPro-Medium.woff -------------------------------------------------------------------------------- /src/components/common/posts/index.js: -------------------------------------------------------------------------------- 1 | export { default as RecentPosts } from './RecentPosts' 2 | export { default as PostAuthor } from './PostAuthor' 3 | -------------------------------------------------------------------------------- /static/fonts/AvenirNextLTPro-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/AvenirNextLTPro-Medium.woff2 -------------------------------------------------------------------------------- /static/fonts/AvenirNextLTPro-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/AvenirNextLTPro-Regular.woff -------------------------------------------------------------------------------- /static/fonts/AvenirNextLTPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/HEAD/static/fonts/AvenirNextLTPro-Regular.woff2 -------------------------------------------------------------------------------- /src/styles/post/index.less: -------------------------------------------------------------------------------- 1 | @import '../variables.less'; 2 | @import '../mixins.less'; 3 | 4 | @import './post.less'; 5 | @import './kg.less'; 6 | @import './relatedposts.less'; 7 | -------------------------------------------------------------------------------- /src/components/sidebar/index.js: -------------------------------------------------------------------------------- 1 | export { default as Sidebar } from './Sidebar' 2 | // export { default as TwitterWidget } from './TwitterWidget' 3 | export { default as AuthorWidget } from './AuthorWidget' 4 | -------------------------------------------------------------------------------- /src/components/common/navigation/index.js: -------------------------------------------------------------------------------- 1 | export { default as Hamburger } from './Hamburger' 2 | export { default as Navigation } from './Navigation' 3 | export { default as NavigationLinks } from './NavigationLinks' 4 | -------------------------------------------------------------------------------- /plugins/gatsby-plugin-ghost-manifest/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "babel-preset-gatsby-package", 5 | { 6 | "browser": true 7 | } 8 | ] 9 | ] 10 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /static/images/counter.svg: -------------------------------------------------------------------------------- 1 | Profile views103395 -------------------------------------------------------------------------------- /src/styles/footer.less: -------------------------------------------------------------------------------- 1 | /* Footer 2 | /* ---------------------------------------------------------- */ 3 | .site-foot { 4 | padding: 40px 0; 5 | background: #fff; 6 | box-shadow: @shadow; 7 | color: rgba(53, 53, 53, 0.7); 8 | font-size: 1.3rem; 9 | 10 | .footer-text { 11 | max-width: 90%; 12 | margin: auto; 13 | line-height: 1.4; 14 | text-align: center; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/app.less: -------------------------------------------------------------------------------- 1 | @import './variables.less'; 2 | @import './mixins.less'; 3 | @import './global.less'; 4 | @import './sidebar.less'; 5 | @import './navigation.less'; 6 | @import './footer.less'; 7 | @import './page.less'; 8 | @import './layout.less'; 9 | @import './index.less'; 10 | @import './tag.less'; 11 | @import './author.less'; 12 | @import './pagination.less'; 13 | @import './hamburger.less'; 14 | @import './content.less'; 15 | -------------------------------------------------------------------------------- /src/components/common/index.js: -------------------------------------------------------------------------------- 1 | export { default as Layout } from './Layout' 2 | export { default as PostCard } from './PostCard' 3 | export { default as Pagination } from './Pagination' 4 | export { default as Navigation } from './Navigation' 5 | export { default as NavigationLinks } from './NavigationLinks' 6 | export { default as Links } from './Links' 7 | export { default as Menu } from './Hamburger' 8 | export { default as Footer } from './Footer' 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /static/images/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import { Layout } from '../components/common' 4 | 5 | const NotFoundPage = () => ( 6 | 7 |
8 |
9 |

Error 404

10 |
11 |

Page not found, return home to start over.

12 |
13 |
14 |
15 |
16 | ) 17 | 18 | export default NotFoundPage 19 | -------------------------------------------------------------------------------- /src/components/sidebar/AuthorWidget.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Link, StaticQuery, graphql } from 'gatsby' 5 | 6 | const AuthorWidget = ({ data }) => { 7 | const site = data.allGhostSettings.edges[0].node 8 | 9 | return ( 10 | <> 11 |
12 | 13 | {site.logo ? {site.title} :

{site.title}

} 14 | 15 |

{site.description}

16 |
17 | 18 | ) 19 | } 20 | 21 | export default AuthorWidget 22 | -------------------------------------------------------------------------------- /src/styles/variables.less: -------------------------------------------------------------------------------- 1 | /* Variables 2 | /* ---------------------------------------------------------- */ 3 | 4 | /* Colours */ 5 | @color-primary: #b15d5d; 6 | @color-base: #15171A; 7 | @color-secondary: #8096a2; 8 | @color-border: #c7d5d8; 9 | @color-bg: #f5f5f5; 10 | @color-content-title: #51566a; 11 | 12 | /* Fonts */ 13 | @font-body: 'FFMarkWebProBook', Helvetica, Sans-Serif; 14 | @font-title: 'FFMarkWebProMedium', Helvetica, Sans-Serif; 15 | @font-mono: Menlo, Courier, monospace; 16 | 17 | /* Breakpoints */ 18 | @mobile-breakpoint: 600px; 19 | @tablet-breakpoint: 800px; 20 | 21 | /* Transitions */ 22 | @transition: all 0.2s ease-out; 23 | @shadow: 0 0 10px #e8e9ef; 24 | -------------------------------------------------------------------------------- /src/components/common/navigation/Hamburger.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { slide as Menu } from "react-burger-menu" 3 | import { Link } from 'gatsby' 4 | import { NavigationLinks } from '.' 5 | 6 | const Hamburger = ({ data, navClass, props }) => ( 7 | // Pass on our props 8 | 9 | {data.map((navItem, i) => { 10 | if (navItem.url.match(/^\s?http(s?)/gi)) { 11 | return {navItem.label} 12 | } else { 13 | return {navItem.label} 14 | } 15 | })} 16 | ) 17 | 18 | export default Hamburger 19 | -------------------------------------------------------------------------------- /src/styles/tag.less: -------------------------------------------------------------------------------- 1 | /* Tag Archives 2 | /* ---------------------------------------------------------- */ 3 | .tag-header { 4 | margin: 0; 5 | padding: 30px; 6 | border-radius: 8px; 7 | background: white; 8 | box-shadow: 0 0 10px #e8e9ef; 9 | } 10 | 11 | .tag-header h1 { 12 | margin: 0 0 1.5rem; 13 | color: #44495e; 14 | font-size: 1.8em; 15 | font-weight: 100; 16 | } 17 | 18 | .tag-header p { 19 | margin: 0; 20 | color: #44495e; 21 | font-size: 1.6rem; 22 | line-height: 1.5em; 23 | } 24 | @media (max-width: 500px) { 25 | .tag-header { 26 | padding-bottom: 4vw; 27 | border-bottom: @color-bg 1px solid; 28 | } 29 | 30 | .tag-header p { 31 | font-size: 1.7rem; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /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/meta/ImageMeta.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Helmet } from 'react-helmet' 3 | import PropTypes from 'prop-types' 4 | import config from '../../../utils/siteConfig' 5 | 6 | const ImageMeta = ({ image }) => { 7 | if (!image) { 8 | return null 9 | } 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | ImageMeta.propTypes = { 23 | image: PropTypes.string, 24 | } 25 | 26 | export default ImageMeta 27 | -------------------------------------------------------------------------------- /static/images/icons/avatar.svg: -------------------------------------------------------------------------------- 1 | Group 8 -------------------------------------------------------------------------------- /src/styles/pagination.less: -------------------------------------------------------------------------------- 1 | /* Pagination 2 | /* ---------------------------------------------------------- */ 3 | .pagination { 4 | display: flex; 5 | position: relative; 6 | align-items: center; 7 | justify-content: space-between; 8 | margin: 0 0 30px; 9 | } 10 | 11 | .pagination a { 12 | display: inline-block; 13 | padding: 10px 15px; 14 | border: @color-border 1px solid; 15 | border-radius: 3px; 16 | color: @color-secondary; 17 | font-size: 1.4rem; 18 | line-height: 1em; 19 | text-decoration: none; 20 | transition: @transition; 21 | 22 | &:hover { 23 | border: @color-primary 1px solid; 24 | background: @color-primary; 25 | color: white; 26 | } 27 | } 28 | 29 | .pagination-location { 30 | position: absolute; 31 | left: 50%; 32 | width: 100px; 33 | margin-left: -50px; 34 | color: @color-secondary; 35 | font-size: 1.3rem; 36 | text-align: center; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/common/Pagination.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'gatsby' 4 | 5 | const Pagination = ({ pageContext }) => { 6 | const { previousPagePath, nextPagePath, humanPageNumber, numberOfPages } = pageContext 7 | 8 | return ( 9 | 22 | ) 23 | } 24 | 25 | Pagination.propTypes = { 26 | pageContext: PropTypes.object.isRequired, 27 | } 28 | 29 | export default Pagination 30 | -------------------------------------------------------------------------------- /static/css/fonts.css: -------------------------------------------------------------------------------- 1 | @import url("//hello.myfonts.net/count/3b4460"); 2 | 3 | @font-face { 4 | src: url('../fonts/FFMarkWebProMedium.woff2') format('woff2'), url('../fonts/FFMarkWebProMedium.woff') format('woff'); 5 | font-display: swap; 6 | font-family: 'FFMarkWebProMedium'; 7 | } 8 | @font-face { 9 | src: url('../fonts/AvenirNextLTPro-Medium.woff2') format('woff2'), url('../fonts/AvenirNextLTPro-Medium.woff') format('woff'); 10 | font-display: swap; 11 | font-family: 'AvenirNextLTPro-Medium'; 12 | } 13 | @font-face { 14 | src: url('../fonts/FFMarkWebProBook.woff2') format('woff2'), url('../fonts/FFMarkWebProBook.woff') format('woff'); 15 | font-display: swap; 16 | font-family: 'FFMarkWebProBook'; 17 | } 18 | @font-face { 19 | src: url('../fonts/AvenirNextLTPro-Regular.woff2') format('woff2'), url('../fonts/AvenirNextLTPro-Regular.woff') format('woff'); 20 | font-display: swap; 21 | font-family: 'AvenirNextLTPro-Regular'; 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/layout.less: -------------------------------------------------------------------------------- 1 | /* Layout 2 | /* ---------------------------------------------------------- */ 3 | .viewport { 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: space-between; 7 | min-height: 100vh; 8 | margin: 0 auto; 9 | } 10 | 11 | .home-template .viewport, 12 | .tag-template .viewport { 13 | max-width: 90%; 14 | } 15 | 16 | .home-container, 17 | .tag-container { 18 | grid-gap: 60px; 19 | display: grid; 20 | grid-template-columns: 1.5fr 3fr; 21 | max-width: 1020px; 22 | margin: 0 auto; 23 | } 24 | @media (max-width: @tablet-breakpoint) { 25 | .home-container, 26 | .tag-container { 27 | grid-template-columns: 1fr; 28 | max-width: unset; 29 | } 30 | } 31 | 32 | .content { 33 | margin: 0 auto; 34 | font-size: 2rem; 35 | line-height: 1.7em; 36 | } 37 | 38 | .content-body { 39 | display: flex; 40 | flex-direction: column; 41 | font-family: @font-body; 42 | margin-bottom: 40px; 43 | } 44 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/common/navigation/NavigationLinks.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'gatsby' 4 | 5 | const NavigationLinks = ({ data, navClass }) => ( 6 | <> 7 | {data.map((navItem, i) => { 8 | if (navItem.url.match(/^\s?http(s?)/gi)) { 9 | return {navItem.label} 10 | } else { 11 | return {navItem.label} 12 | } 13 | })} 14 | 15 | ) 16 | 17 | NavigationLinks.defaultProps = { 18 | navClass: `site-nav-item`, 19 | navType: `home-nav`, 20 | } 21 | 22 | NavigationLinks.propTypes = { 23 | data: PropTypes.arrayOf( 24 | PropTypes.shape({ 25 | label: PropTypes.string.isRequired, 26 | url: PropTypes.string.isRequired, 27 | }).isRequired, 28 | ).isRequired, 29 | navClass: PropTypes.string, 30 | navType: PropTypes.string, 31 | } 32 | 33 | export default NavigationLinks 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /static/images/icons/rss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/common/Links.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'gatsby' 4 | 5 | /** 6 | * Navigation component 7 | * 8 | * The Navigation component takes an array of your Ghost 9 | * navigation property that is fetched from the settings. 10 | * It differentiates between absolute (external) and relative link (internal). 11 | * You can pass it a custom class for your own styles, but it will always fallback 12 | * to a `site-nav-item` class. 13 | * 14 | */ 15 | const Links = ({ data, navClass }) => ( 16 | <> 17 | {data.map((navItem, i) => { 18 | if (navItem.url.match(/^\s?http(s?)/gi)) { 19 | return {navItem.label} 20 | } else { 21 | return {navItem.label} 22 | } 23 | })} 24 | 25 | ) 26 | 27 | Links.defaultProps = { 28 | navClass: `site-nav-item`, 29 | } 30 | 31 | Links.propTypes = { 32 | data: PropTypes.arrayOf( 33 | PropTypes.shape({ 34 | label: PropTypes.string.isRequired, 35 | url: PropTypes.string.isRequired, 36 | }).isRequired, 37 | ).isRequired, 38 | navClass: PropTypes.string, 39 | } 40 | 41 | export default Links 42 | -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /src/components/common/meta/getAuthorProperties.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import PropTypes from 'prop-types' 3 | 4 | export const getAuthorProperties = (primaryAuthor) => { 5 | let authorProfiles = [] 6 | 7 | authorProfiles.push( 8 | primaryAuthor.website ? primaryAuthor.website : null, 9 | primaryAuthor.twitter ? `https://twitter.com/${primaryAuthor.twitter.replace(/^@/, ``)}/` : null, 10 | primaryAuthor.facebook ? `https://www.facebook.com/${primaryAuthor.facebook.replace(/^\//, ``)}/` : null 11 | ) 12 | 13 | authorProfiles = _.compact(authorProfiles) 14 | 15 | return { 16 | name: primaryAuthor.name || null, 17 | sameAsArray: authorProfiles.length ? `["${_.join(authorProfiles, `", "`)}"]` : null, 18 | image: primaryAuthor.profile_image || null, 19 | facebookUrl: primaryAuthor.facebook ? `https://www.facebook.com/${primaryAuthor.facebook.replace(/^\//, ``)}/` : null, 20 | } 21 | } 22 | 23 | getAuthorProperties.defaultProps = { 24 | fetchAuthorData: false, 25 | } 26 | 27 | getAuthorProperties.PropTypes = { 28 | primaryAuthor: PropTypes.shape({ 29 | name: PropTypes.string.isRequired, 30 | profile_image: PropTypes.string, 31 | website: PropTypes.string, 32 | twitter: PropTypes.string, 33 | facebook: PropTypes.string, 34 | }).isRequired, 35 | } 36 | 37 | export default getAuthorProperties 38 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/common/NavigationLinks.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'gatsby' 4 | 5 | /** 6 | * Navigation component 7 | * 8 | * The Navigation component takes an array of your Ghost 9 | * navigation property that is fetched from the settings. 10 | * It differentiates between absolute (external) and relative link (internal). 11 | * You can pass it a custom class for your own styles, but it will always fallback 12 | * to a `site-nav-item` class. 13 | * 14 | */ 15 | 16 | const NavigationLinks = ({ data, navClass }) => ( 17 | <> 18 | {data.map((navItem, i) => { 19 | if (navItem.url.match(/^\s?http(s?)/gi)) { 20 | return {navItem.label} 21 | } else { 22 | return {navItem.label} 23 | } 24 | })} 25 | 26 | ) 27 | 28 | NavigationLinks.defaultProps = { 29 | navClass: `site-nav-item`, 30 | navType: `home-nav`, 31 | } 32 | 33 | NavigationLinks.propTypes = { 34 | data: PropTypes.arrayOf( 35 | PropTypes.shape({ 36 | label: PropTypes.string.isRequired, 37 | url: PropTypes.string.isRequired, 38 | }).isRequired, 39 | ).isRequired, 40 | navClass: PropTypes.string, 41 | navType: PropTypes.string, 42 | } 43 | 44 | export default NavigationLinks 45 | -------------------------------------------------------------------------------- /src/components/common/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'gatsby' 4 | import { Menu, NavigationLinks } from '.' 5 | 6 | /** 7 | * Navigation component 8 | * 9 | * The Navigation component takes an array of your Ghost 10 | * navigation property that is fetched from the settings. 11 | * It differentiates between absolute (external) and relative link (internal). 12 | * You can pass it a custom class for your own styles, but it will always fallback 13 | * to a `site-nav-item` class. 14 | * 15 | */ 16 | 17 | const Navigation = ({ data, navClass, logo, isHome }) => ( 18 | <> 19 | 25 | { isHome ? null : } 26 | 27 | 28 | ) 29 | 30 | Navigation.defaultProps = { 31 | navClass: `site-nav-item`, 32 | navType: `home-nav`, 33 | } 34 | 35 | Navigation.propTypes = { 36 | data: PropTypes.arrayOf( 37 | PropTypes.shape({ 38 | label: PropTypes.string.isRequired, 39 | url: PropTypes.string.isRequired, 40 | }).isRequired, 41 | ).isRequired, 42 | navClass: PropTypes.string, 43 | navType: PropTypes.string, 44 | } 45 | 46 | export default Navigation 47 | -------------------------------------------------------------------------------- /src/styles/post/relatedposts.less: -------------------------------------------------------------------------------- 1 | .recent-posts, 2 | .related-posts { 3 | display: flex; 4 | justify-content: space-between; 5 | margin-top: 20px; 6 | @media(max-width: @mobile-breakpoint) { 7 | display: block; 8 | } 9 | 10 | .recent-post-card, 11 | .related-post-card { 12 | width: 32%; 13 | overflow: hidden; 14 | border-radius: 5px; 15 | background: white; 16 | box-shadow: 0 0 10px #e8e9ef; 17 | transition: all 0.2s ease-out; 18 | @media(max-width: @mobile-breakpoint) { 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-between; 22 | width: 100%; 23 | margin-bottom: 10px; 24 | 25 | img { 26 | width: 33%; 27 | height: 77px; 28 | } 29 | } 30 | 31 | &:hover { 32 | background: @color-primary; 33 | color: white !important; 34 | text-decoration: none; 35 | 36 | * { 37 | color: white; 38 | } 39 | } 40 | 41 | .recent-post-title, 42 | .related-post-title { 43 | width: -webkit-fill-available; 44 | margin: 0; 45 | padding: 20px 15px; 46 | line-height: 1.3; 47 | @media(max-width: @mobile-breakpoint) { 48 | padding: 15px; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/siteConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | siteUrl: `https://toddbirchard.com`, // Site domain. Do not include a trailing slash! 3 | siteRss: `https://toddbirchard.com/rss.xml`, 4 | siteMap: `https://toddbirchard.com/sitemap.xml`, 5 | siteAdminUrl: `https://toddbirchard.app`, 6 | 7 | // Post confit=g 8 | postsPerPage: 8, 9 | 10 | // Metadata 11 | siteTitleMeta: `Todd Birchard: Engineering, Product, Technology.`, 12 | shortTitle: `Todd Birchard`, 13 | siteDescriptionMeta: `Giant reptile giving technology a good name. Occasional tangents of mass destruction. Made in Silicon Alley.`, 14 | categories: [`software`, `engineering`, `data`, `data science`, `data engineering`], 15 | siteCopyright: `©2021 Todd Birchard: Engineering, Product, Technology.`, 16 | backgroundColor: `#f8f8f8`, 17 | themeColor: `#b15d5d`, 18 | 19 | // Image Config 20 | images: { 21 | siteIcon: `favicon.png`, 22 | mobileLogo: `/images/logo@2x.png`, 23 | buyMeACoffee: `/images/buymeacoffee.svg`, 24 | shareImage: `cover.jpg`, 25 | shareImageWidth: 1000, 26 | shareImageHeight: 523, 27 | }, 28 | 29 | // Creator information 30 | creator: { 31 | name: `Todd Birchard`, 32 | twitter: `@toddrbirchard`, 33 | }, 34 | 35 | // Site social media 36 | links: { 37 | twitter: `https://twitter.com/hackersslackers`, 38 | buyMeACoffee: `https://buymeacoff.ee/hackersslackers`, 39 | githubOrg: `https://github.com/hackersandslackers/`, 40 | }, 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/components/common/navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'gatsby' 4 | import { Hamburger, NavigationLinks } from '.' 5 | 6 | /** 7 | * Navigation component 8 | * 9 | * The Navigation component takes an array of your Ghost 10 | * navigation property that is fetched from the settings. 11 | * It differentiates between absolute (external) and relative link (internal). 12 | * You can pass it a custom class for your own styles, but it will always fallback 13 | * to a `site-nav-item` class. 14 | * 15 | */ 16 | 17 | const Navigation = ({ data, navClass, logo, isHome }) => ( 18 | <> 19 | 25 | { isHome ? null : } 26 | 27 | 28 | ) 29 | 30 | Navigation.defaultProps = { 31 | navClass: `site-nav-item`, 32 | navType: `home-nav`, 33 | } 34 | 35 | Navigation.propTypes = { 36 | data: PropTypes.arrayOf( 37 | PropTypes.shape({ 38 | label: PropTypes.string.isRequired, 39 | url: PropTypes.string.isRequired, 40 | }).isRequired, 41 | ).isRequired, 42 | isHome: PropTypes.string, 43 | logo: PropTypes.string.isRequired, 44 | navClass: PropTypes.string, 45 | navType: PropTypes.string, 46 | } 47 | 48 | export default Navigation 49 | -------------------------------------------------------------------------------- /src/components/common/posts/RecentPosts.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { StaticQuery, graphql } from 'gatsby' 4 | import { Link } from 'gatsby' 5 | 6 | const RecentPosts = ({ data }) => { 7 | const posts = data.allGhostPost.edges 8 | 9 | return ( 10 | <> 11 |
12 | {posts.map(({ node }) => ( 13 | 14 | 15 |
{ node.title }
16 | 17 | ))} 18 |
19 | 20 | ) 21 | } 22 | 23 | RecentPosts.propTypes = { 24 | data: PropTypes.shape({ 25 | allGhostPost: PropTypes.object.isRequired, 26 | }).isRequired, 27 | } 28 | 29 | const RecentPostsQuery = props => ( 30 | } 46 | /> 47 | ) 48 | 49 | export default RecentPostsQuery 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/common/posts/PostAuthor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { FaUserEdit, FaGlobe, FaTwitter } from 'react-icons/fa' 5 | 6 | /** 7 | * Single post view (/:slug) 8 | * 9 | * This file renders a single post and loads all the content. 10 | * 11 | */ 12 | 13 | const PostAuthor = ({ author }) => { 14 | const authorTwitterUrl = author.twitter ? `https://twitter.com/${author.twitter.replace(/^@/, ``)}` : null 15 | 16 | return ( 17 | <> 18 |
19 |
20 |

{author.name}

21 | {author.bio &&

{author.bio}

} 22 |
23 | {author.website && Website} 24 | {authorTwitterUrl && Twitter} 25 |
26 |
27 |
28 | {author.profile_image && {author.name}} 29 |
30 |
31 | 32 | ) 33 | } 34 | 35 | PostAuthor.propTypes = { 36 | author: PropTypes.shape({ 37 | name: PropTypes.string.isRequired, 38 | url: PropTypes.string.isRequired, 39 | bio: PropTypes.string.isRequired, 40 | profile_image: PropTypes.string.isRequired, 41 | website: PropTypes.string, 42 | twitter: PropTypes.string, 43 | }).isRequired, 44 | } 45 | 46 | export default PostAuthor 47 | -------------------------------------------------------------------------------- /src/templates/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { graphql } from 'gatsby' 4 | import { Layout, PostCard, Pagination } from '../components/common' 5 | import { MetaData } from '../components/common/meta' 6 | 7 | /** 8 | * Home page 9 | */ 10 | 11 | const Index = ({ data, location, pageContext }) => { 12 | const posts = data.allGhostPost.edges 13 | 14 | return ( 15 | <> 16 | 17 | 18 |
19 |
20 | {posts.map(({ node }) => ( 21 | 22 | ))} 23 | 24 |
25 |
26 | 27 |
28 | 29 | ) 30 | } 31 | 32 | Index.propTypes = { 33 | data: PropTypes.shape({ 34 | allGhostPost: PropTypes.object.isRequired, 35 | }).isRequired, 36 | location: PropTypes.shape({ 37 | pathname: PropTypes.string.isRequired, 38 | }).isRequired, 39 | pageContext: PropTypes.object, 40 | } 41 | 42 | export default Index 43 | 44 | // This page query loads all posts sorted descending by published date 45 | // The `limit` and `skip` values are used for pagination 46 | export const pageQuery = graphql` 47 | query GhostPostQuery($limit: Int!, $skip: Int!) { 48 | allGhostPost( 49 | sort: { order: DESC, fields: [published_at] }, 50 | limit: $limit, 51 | skip: $skip 52 | ) { 53 | edges { 54 | node { 55 | ...GhostPostFields 56 | } 57 | } 58 | } 59 | } 60 | ` 61 | -------------------------------------------------------------------------------- /src/styles/hamburger.less: -------------------------------------------------------------------------------- 1 | .bm-menu { 2 | padding: 2.5em 1.5em 0; 3 | background: @color-primary; 4 | font-size: 1.15em; 5 | 6 | .bm-item-list { 7 | position: absolute; 8 | top: 0; 9 | right: 0; 10 | bottom: 0; 11 | left: 0; 12 | height: fit-content !important; 13 | margin: auto; 14 | color: #fff; 15 | opacity: 1; 16 | 17 | .bm-item { 18 | display: inline-block; 19 | width: fit-content; 20 | margin: 0 auto 20px; 21 | margin-bottom: 30px; 22 | outline-color: transparent; 23 | outline-style: none; 24 | color: #fff; 25 | font-size: 1.3em !important; 26 | font-weight: 400; 27 | text-decoration: none; 28 | transition: color 0.2s; 29 | 30 | &:hover { 31 | color: white; 32 | } 33 | } 34 | } 35 | } 36 | 37 | .bm-burger-button { 38 | display: none; 39 | position: absolute; 40 | top: 29px; 41 | right: 35px; 42 | width: 32px; 43 | height: 27px; 44 | outline-color: transparent; 45 | outline-style: none; 46 | 47 | .bm-burger-bars { 48 | height: 15% !important; 49 | background: #373a47; 50 | } 51 | } 52 | 53 | .bm-cross-button { 54 | top: 13px !important; 55 | right: 20px !important; 56 | width: 34px !important; 57 | height: 34px !important; 58 | 59 | .bm-cross { 60 | height: 30px !important; 61 | background: #fff; 62 | } 63 | } 64 | 65 | .bm-morph-shape { 66 | fill: #373a47; 67 | } 68 | 69 | .bm-overlay { 70 | background: rgba(0, 0, 0, 0.3); 71 | } 72 | @media(max-width: @mobile-breakpoint) { 73 | .page-template .bm-burger-button, 74 | .post-template .bm-burger-button { 75 | display: block; 76 | } 77 | } 78 | 79 | .tag-template .bm-menu-wrap { 80 | display: none; 81 | } 82 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/common/PostCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link } from 'gatsby' 4 | import { Tags } from '@tryghost/helpers-gatsby' 5 | import { readingTime as readingTimeHelper } from '@tryghost/helpers' 6 | import { FaTag, FaEye } from 'react-icons/fa' 7 | 8 | const PostCard = ({ post }) => { 9 | const url = `/${ post.slug }/` 10 | const readingTime = readingTimeHelper(post) 11 | 12 | return ( 13 | { 14 | post.feature_image &&
17 | } 18 | {post.featured && Featured} 19 |
20 |

{post.title}

21 |
{post.excerpt}
22 |
23 |
24 |
25 | 26 | {post.tags && } 27 |
28 |
29 | 30 | {readingTime} 31 |
32 |
33 |
34 |
35 | ) 36 | } 37 | 38 | PostCard.propTypes = { 39 | post: PropTypes.shape({ 40 | slug: PropTypes.string.isRequired, 41 | title: PropTypes.string.isRequired, 42 | feature_image: PropTypes.string, 43 | featured: PropTypes.bool, 44 | tags: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string })), 45 | excerpt: PropTypes.string.isRequired, 46 | primary_author: PropTypes.shape({ name: PropTypes.string.isRequired, 47 | profile_image: PropTypes.string }).isRequired, 48 | }).isRequired, 49 | } 50 | 51 | export default PostCard 52 | -------------------------------------------------------------------------------- /src/templates/page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { graphql } from 'gatsby' 4 | import { Helmet } from 'react-helmet' 5 | 6 | import { Layout } from '../components/common' 7 | import { MetaData } from '../components/common/meta' 8 | 9 | /** 10 | * Single page (/:slug) 11 | * 12 | * This file renders a single page and loads all the content. 13 | * 14 | */ 15 | const Page = ({ data, location, pageContext }) => { 16 | const page = data.ghostPage 17 | 18 | return ( 19 | <> 20 | 26 | 27 | 28 | 29 | 30 |
31 | { page.feature_image ? 32 |
33 | { 34 |
: null } 35 |
36 |

{page.title}

37 | 38 | {/* The main page content */} 39 |
43 |
44 |
45 |
46 | 47 | ) 48 | } 49 | 50 | Page.propTypes = { 51 | data: PropTypes.shape({ 52 | ghostPage: PropTypes.shape({ 53 | title: PropTypes.string.isRequired, 54 | html: PropTypes.string.isRequired, 55 | feature_image: PropTypes.string, 56 | codeinjection_styles: PropTypes.string, 57 | }).isRequired, 58 | }).isRequired, 59 | location: PropTypes.object.isRequired, 60 | } 61 | 62 | export default Page 63 | 64 | export const postQuery = graphql` 65 | query($slug: String!) { 66 | ghostPage(slug: { eq: $slug }) { 67 | ...GhostPageFields 68 | } 69 | } 70 | ` 71 | -------------------------------------------------------------------------------- /static/images/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/author.less: -------------------------------------------------------------------------------- 1 | /* Author Archives 2 | /* ---------------------------------------------------------- */ 3 | .author-header { 4 | display: flex; 5 | justify-content: space-between; 6 | margin: 0 0 4vw; 7 | @media (max-width: 500px) { 8 | padding-bottom: 4vw; 9 | border-bottom: @color-bg 1px solid; 10 | } 11 | 12 | h1 { 13 | margin: 0 0 0.5rem; 14 | } 15 | 16 | p { 17 | margin: 0; 18 | color: @color-secondary; 19 | font-size: 2.2rem; 20 | line-height: 1.3em; 21 | @media (max-width: 500px) { 22 | font-size: 1.7rem; 23 | } 24 | } 25 | } 26 | 27 | .author-header-image { 28 | flex: 0 0 auto; 29 | width: 120px; 30 | height: 120px; 31 | margin: 0 0 0 4vw; 32 | overflow: hidden; 33 | border-radius: 100%; 34 | @media (max-width: 500px) { 35 | width: 80px; 36 | height: 80px; 37 | } 38 | } 39 | 40 | .author-header-meta { 41 | display: flex; 42 | margin: 1rem 0 0; 43 | } 44 | 45 | .author-header-item { 46 | display: block; 47 | padding: 2px 10px; 48 | 49 | &:first-child { 50 | padding-left: 0; 51 | } 52 | } 53 | /* Post Author Card 54 | /* ---------------------------------------------------------- */ 55 | .post-author { 56 | display: flex; 57 | margin: 40px 0 80px; 58 | padding-top: 40px; 59 | border-top: 1px solid #e2e2e2; 60 | 61 | .post-author-content { 62 | margin-right: 20px; 63 | } 64 | 65 | .post-author-name { 66 | margin: 0 0 0.5em; 67 | color: @color-content-title; 68 | font-family: @font-title; 69 | font-size: 1.7em; 70 | } 71 | 72 | .post-author-bio { 73 | margin: 0 0 1em; 74 | line-height: 1.4em; 75 | } 76 | 77 | .post-author-image { 78 | width: 116px; 79 | min-width: 116px; 80 | height: 116px; 81 | overflow: hidden; 82 | border-radius: 50%; 83 | @media(max-width: @mobile-breakpoint) { 84 | width: 80px; 85 | min-width: 80px; 86 | height: 80px; 87 | } 88 | } 89 | 90 | .post-author-item { 91 | margin-right: 20px; 92 | color: @color-secondary; 93 | font-size: 0.9em; 94 | opacity: 0.7; 95 | 96 | svg { 97 | margin-right: 5px; 98 | font-size: 0.9em; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | }; -------------------------------------------------------------------------------- /src/styles/post/post.less: -------------------------------------------------------------------------------- 1 | /* Posts 2 | /* ---------------------------------------------------------- */ 3 | .post-feature-image { 4 | margin-bottom: 30px; 5 | 6 | img { 7 | -o-object-fit: cover; 8 | width: 100%; 9 | height: 500px; 10 | object-fit: cover; 11 | @media(max-width: @mobile-breakpoint) { 12 | height: 300px; 13 | } 14 | } 15 | } 16 | 17 | .post-meta { 18 | margin-bottom: 2em; 19 | line-height: 1; 20 | 21 | .meta-item { 22 | display: inline-block; 23 | margin-right: 40px; 24 | font-size: 0.7em; 25 | font-weight: 600; 26 | line-height: 1; 27 | opacity: 0.6; 28 | @media(max-width: @mobile-breakpoint) { 29 | display: block; 30 | margin-right: 30px; 31 | line-height: 1.5; 32 | } 33 | 34 | a { 35 | color: unset; 36 | } 37 | 38 | svg { 39 | margin-right: 7px; 40 | font-size: 0.8em; 41 | } 42 | } 43 | } 44 | 45 | .post-full-content { 46 | width: 750px; 47 | max-width: 80%; 48 | margin: 0 auto; 49 | @media (max-width: @tablet-breakpoint) { 50 | max-width: 85%; 51 | } 52 | @media(max-width: @mobile-breakpoint) { 53 | max-width: 90%; 54 | } 55 | 56 | p:last-of-type { 57 | margin-bottom: 0; 58 | } 59 | 60 | img { 61 | width: 100%; 62 | margin: 0 0 3vw; 63 | object-fit: cover; 64 | @media(max-width: @tablet-breakpoint) { 65 | height: 300px; 66 | } 67 | } 68 | } 69 | 70 | .post-tags .tag { 71 | display: inline-block; 72 | margin: 0 8px 8px 0; 73 | padding: 6px 14px; 74 | border: 1px solid #e1e1e1; 75 | border-radius: 4px; 76 | background: #fff; 77 | color: #797979; 78 | font-size: 0.8em; 79 | line-height: 1.5; 80 | text-decoration: none; 81 | transition: 0.2s all ease-out; 82 | 83 | &:hover { 84 | border-color: #b15d5d; 85 | background: #b15d5d; 86 | color: white; 87 | } 88 | } 89 | 90 | .post-footer { 91 | width: 750px; 92 | max-width: 80%; 93 | margin: 0 auto; 94 | @media(max-width: 600px) { 95 | width: unset; 96 | max-width: 90% 97 | } 98 | } 99 | 100 | .post-tags a { 101 | display: inline-block; 102 | margin-right: 5px; 103 | color: @color-secondary; 104 | opacity: 0.7; 105 | } 106 | 107 | .post-footer { 108 | .post-social { 109 | display: flex; 110 | align-items: center; 111 | justify-content: space-between; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/templates/tag.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { graphql } from 'gatsby' 4 | import { Layout, PostCard, Pagination } from '../components/common' 5 | import { MetaData } from '../components/common/meta' 6 | import { Sidebar } from '../components/sidebar/' 7 | 8 | /** 9 | * Tag page 10 | */ 11 | 12 | const Tag = ({ data, location, pageContext }) => { 13 | const tag = data.ghostTag 14 | const posts = data.allGhostPost.edges 15 | 16 | return ( 17 | <> 18 | 23 | 24 |
25 | 26 |
27 |
28 |

{tag.name}

29 | {tag.description ?

{tag.description}

: null } 30 |
31 | {posts.map(({ node }) => ( 32 | // The tag below includes the markup for each post - components/common/PostCard.js 33 | 34 | ))} 35 | 36 |
37 | 38 |
39 | 40 |
41 | 42 | ) 43 | } 44 | 45 | Tag.propTypes = { 46 | data: PropTypes.shape({ 47 | ghostTag: PropTypes.shape({ 48 | name: PropTypes.string.isRequired, 49 | description: PropTypes.string, 50 | }), 51 | allGhostPost: PropTypes.object.isRequired, 52 | }).isRequired, 53 | location: PropTypes.shape({ 54 | pathname: PropTypes.string.isRequired, 55 | }).isRequired, 56 | pageContext: PropTypes.object, 57 | icon: PropTypes.string, 58 | } 59 | 60 | export default Tag 61 | 62 | export const pageQuery = graphql` 63 | query GhostTagQuery($slug: String!, $limit: Int!, $skip: Int!) { 64 | ghostTag(slug: { eq: $slug }) { 65 | ...GhostTagFields 66 | } 67 | allGhostPost( 68 | sort: { order: DESC, fields: [published_at] }, 69 | filter: {tags: {elemMatch: {slug: {eq: $slug}}}}, 70 | limit: $limit, 71 | skip: $skip 72 | ) { 73 | edges { 74 | node { 75 | ...GhostPostFields 76 | } 77 | } 78 | } 79 | allGhostSettings { 80 | edges { 81 | node { 82 | icon 83 | } 84 | } 85 | } 86 | } 87 | ` 88 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | /* Index 2 | /* ---------------------------------------------------------- */ 3 | .post-feed { 4 | grid-gap: 30px; 5 | display: grid; 6 | grid-auto-rows: min-content; 7 | grid-template-columns: 1fr; 8 | } 9 | 10 | .post-card { 11 | overflow: hidden; 12 | border-radius: 8px; 13 | background-color: #fff; 14 | box-shadow: 0 0 10px #e8e9ef; 15 | @media(max-width: @tablet-breakpoint) { 16 | padding: 0; 17 | } 18 | 19 | &:hover { 20 | text-decoration: none; 21 | 22 | .post-card-image { 23 | opacity: 0.7; 24 | } 25 | } 26 | 27 | .post-card-image { 28 | width: auto; 29 | height: 300px; 30 | margin: 0 0 10px; 31 | background: @color-secondary no-repeat center center; 32 | background-size: cover; 33 | transition: opacity 0.2s ease-out; 34 | @media(max-width: @mobile-breakpoint) { 35 | height: 225px; 36 | } 37 | } 38 | 39 | .post-card-detail { 40 | padding: 10px 25px; 41 | 42 | .post-card-title { 43 | margin: 0 0 15px; 44 | padding: 0; 45 | color: #44495e; 46 | font-size: 2em; 47 | letter-spacing: 0; 48 | @media(max-width: @mobile-breakpoint) { 49 | font-size: 1.7em; 50 | } 51 | } 52 | 53 | .post-card-excerpt { 54 | color: #44495e; 55 | font-size: 1.15em; 56 | font-weight: 400; 57 | line-height: 1.3em; 58 | } 59 | } 60 | 61 | .meta-item { 62 | margin-right: 30px; 63 | color: @color-secondary; 64 | font-weight: 600; 65 | display: flex; 66 | align-items: center; 67 | 68 | svg { 69 | margin-right: 7px; 70 | font-size: 0.9em; 71 | } 72 | } 73 | } 74 | 75 | .post-card-tags { 76 | margin: 0 0 5px; 77 | color: @color-secondary; 78 | font-size: 1.4rem; 79 | line-height: 1.15em; 80 | } 81 | 82 | .post-card-footer { 83 | display: flex; 84 | align-items: center; 85 | justify-content: space-between; 86 | margin: 30px 0 10px; 87 | color: @color-secondary; 88 | 89 | .post-card-footer-left { 90 | display: flex; 91 | align-items: center; 92 | } 93 | } 94 | 95 | .post-card-avatar { 96 | display: flex; 97 | align-items: center; 98 | justify-content: center; 99 | width: 30px; 100 | height: 30px; 101 | margin: 0 7px 0 0; 102 | border-radius: 100%; 103 | 104 | .author-profile-image { 105 | display: block; 106 | width: 100%; 107 | object-fit: cover; 108 | border-radius: 100%; 109 | background: @color-secondary; 110 | } 111 | 112 | .default-avatar { 113 | width: 26px; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/styles/content.less: -------------------------------------------------------------------------------- 1 | .content-body { 2 | h1, 3 | h2, 4 | h3, 5 | h4, 6 | h5, 7 | h6 { 8 | font-family: @font-body; 9 | color: @color-content-title; 10 | font-family: @font-body; 11 | } 12 | 13 | h1 { 14 | margin: 1.0em 0 0.5em; 15 | font-size: 3.4rem; 16 | font-weight: 700; 17 | @media (max-width: 500px) { 18 | font-size: 2.8rem; 19 | } 20 | } 21 | 22 | h2 { 23 | margin: 0.8em 0 0.4em; 24 | color: @color-content-title; 25 | font-size: 3rem; 26 | font-weight: 700; 27 | @media (max-width: @tablet-breakpoint) { 28 | font-size: 2.6rem; 29 | } 30 | } 31 | 32 | h3 { 33 | margin: 0.5em 0 0.2em; 34 | font-size: 1em; 35 | font-weight: 700; 36 | @media (max-width: 500px) { 37 | font-size: 2.2rem; 38 | } 39 | } 40 | 41 | h4 { 42 | margin: 0.5em 0 0.2em; 43 | font-size: 2.4rem; 44 | font-weight: 700; 45 | @media (max-width: 500px) { 46 | font-size: 2.2rem; 47 | } 48 | } 49 | 50 | h5 { 51 | display: block; 52 | margin: 0.5em 0; 53 | padding: 1em 0 1.5em; 54 | border: 0; 55 | color: @color-primary; 56 | font-family: Georgia, serif; 57 | font-size: 3.2rem; 58 | font-style: italic; 59 | line-height: 1.35em; 60 | text-align: center; 61 | } 62 | 63 | h6 { 64 | margin: 0.5em 0 0.2em; 65 | font-size: 2.0rem; 66 | font-weight: 700; 67 | } 68 | 69 | figure { 70 | margin: 0.4em 0 1.6em; 71 | overflow: hidden; 72 | border-radius: 3px; 73 | font-size: 2.8rem; 74 | font-weight: 700; 75 | 76 | figcaption { 77 | margin: -5px 0 0; 78 | padding: 10px; 79 | background: #fafafa; 80 | color: #5a5a5a; 81 | font-size: 14px; 82 | font-weight: 100; 83 | font-family: @font-body; 84 | text-align: center; 85 | } 86 | } 87 | 88 | pre { 89 | margin: 0.4em 0 1.8em; 90 | padding: 20px; 91 | border-radius: 12px; 92 | background: @color-base; 93 | color: #fff; 94 | font-size: 1.6rem; 95 | line-height: 1.4em; 96 | white-space: pre-wrap; 97 | } 98 | 99 | blockquote, 100 | li, 101 | p { 102 | font-size: 0.8em; 103 | line-height: 1.5; 104 | } 105 | 106 | hr { 107 | margin: 0 0 1.5em; 108 | } 109 | 110 | iframe { 111 | width: 100%; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/templates/author.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { graphql } from 'gatsby' 4 | import { Layout, PostCard, Pagination } from '../components/common' 5 | import { PostAuthor } from '../components/common/posts' 6 | import { MetaData } from '../components/common/meta' 7 | 8 | /** 9 | * Author page 10 | */ 11 | 12 | const Author = ({ data, location, pageContext }) => { 13 | const author = data.ghostAuthor 14 | const posts = data.allGhostPost.edges 15 | const twitterUrl = author.twitter ? `https://twitter.com/${author.twitter.replace(/^@/, ``)}` : null 16 | 17 | return ( 18 | <> 19 | 24 | 25 |
26 |
27 | { author.cover_image ? 28 |
29 | { 30 |
: null } 31 |
32 | {/*

{author.name}

*/} 33 | 34 |
35 | {posts.map(({ node }) => ( 36 | // The tag below includes the markup for each post - components/common/PostCard.js 37 | 38 | ))} 39 |
40 | 41 |
42 |
43 |
44 |
45 | 46 | ) 47 | } 48 | 49 | Author.propTypes = { 50 | data: PropTypes.shape({ 51 | ghostAuthor: PropTypes.shape({ 52 | name: PropTypes.string.isRequired, 53 | cover_image: PropTypes.string, 54 | profile_image: PropTypes.string, 55 | website: PropTypes.string, 56 | bio: PropTypes.string, 57 | location: PropTypes.string, 58 | twitter: PropTypes.string, 59 | }), 60 | allGhostPost: PropTypes.object.isRequired, 61 | }).isRequired, 62 | location: PropTypes.shape({ 63 | pathname: PropTypes.string.isRequired, 64 | }).isRequired, 65 | pageContext: PropTypes.object, 66 | } 67 | 68 | export const pageQuery = graphql` 69 | query GhostAuthorQuery($slug: String!, $limit: Int!, $skip: Int!) { 70 | ghostAuthor(slug: { eq: $slug }) { 71 | ...GhostAuthorFields 72 | } 73 | allGhostPost( 74 | sort: { order: DESC, fields: [published_at] }, 75 | filter: {authors: {elemMatch: {slug: {eq: $slug}}}}, 76 | limit: $limit, 77 | skip: $skip 78 | ) { 79 | edges { 80 | node { 81 | ...GhostPostFields 82 | } 83 | } 84 | } 85 | }` 86 | 87 | export default Author 88 | -------------------------------------------------------------------------------- /src/components/sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { Link, StaticQuery, graphql } from 'gatsby' 4 | import { FaRss, 5 | FaTwitter, 6 | FaAngellist, 7 | FaLinkedinIn, 8 | FaGithubAlt, 9 | FaQuora, 10 | FaMedium } from 'react-icons/fa' 11 | // import { TwitterWidget } from './' 12 | 13 | const Sidebar = ({ data }) => { 14 | const site = data.allGhostSettings.edges[0].node 15 | const twitterUrl = site.twitter ? `https://twitter.com/${site.twitter.replace(/^@/, ``)}` : null 16 | const publicTags = data.allGhostTag.edges 17 | 18 | return ( 19 | <> 20 | 45 | 46 | ) 47 | } 48 | 49 | Sidebar.propTypes = { 50 | bodyClass: PropTypes.string, 51 | isHome: PropTypes.bool, 52 | data: PropTypes.shape({ 53 | allGhostSettings: PropTypes.object.isRequired, 54 | allGhostTag: PropTypes.object.isRequired, 55 | }).isRequired, 56 | } 57 | 58 | const SidebarQuery = props => ( 59 | } 82 | /> 83 | ) 84 | 85 | export default SidebarQuery 86 | -------------------------------------------------------------------------------- /src/styles/post/kg.less: -------------------------------------------------------------------------------- 1 | .kg-width-wide { 2 | position: relative; 3 | width: 85vw; 4 | min-width: 100%; 5 | margin: auto calc(50% - 50vw); 6 | transform: translateX(calc(50vw - 50%)); 7 | } 8 | 9 | .kg-width-full { 10 | position: relative; 11 | right: 50%; 12 | left: 50%; 13 | width: 100vw; 14 | margin-right: -50vw; 15 | margin-left: -50vw; 16 | } 17 | 18 | .kg-width-wide img { 19 | max-width: 85vw; 20 | } 21 | 22 | .kg-width-full img { 23 | max-width: 100vw; 24 | } 25 | 26 | .kg-width-wide { 27 | position: relative; 28 | width: 85vw; 29 | min-width: 100%; 30 | margin: auto calc(50% - 50vw); 31 | transform: translateX(calc(50vw - 50%)); 32 | } 33 | 34 | .kg-width-full { 35 | position: relative; 36 | right: 50%; 37 | left: 50%; 38 | width: 100vw; 39 | margin-right: -50vw; 40 | margin-left: -50vw; 41 | } 42 | 43 | .kg-bookmark-container { 44 | display: flex; 45 | overflow: hidden; 46 | border: 1px solid #ececec; 47 | border-radius: 5px; 48 | background: white; 49 | color: #44495e; 50 | align-items: center; 51 | text-decoration: none !important; 52 | transition: @transition; 53 | @media(max-width: @mobile-breakpoint) { 54 | flex-direction: column-reverse; 55 | } 56 | 57 | &:hover { 58 | background: @color-primary; 59 | color: white; 60 | 61 | * { 62 | color: white !important; 63 | } 64 | } 65 | } 66 | 67 | .kg-bookmark-content { 68 | flex-basis: 0; 69 | flex-grow: 999; 70 | min-width: 50%; 71 | padding: 20px; 72 | } 73 | 74 | .kg-bookmark-title { 75 | color: @color-content-title; 76 | font-size: 0.7em; 77 | line-height: 1; 78 | text-decoration: none; 79 | } 80 | 81 | .kg-bookmark-description { 82 | margin: 15px 0; 83 | } 84 | 85 | .kg-bookmark-metadata { 86 | margin-top: 12px; 87 | } 88 | 89 | .kg-bookmark-metadata { 90 | display: flex; 91 | align-items: center; 92 | margin-top: 12px; 93 | font-size: 0.5em; 94 | line-height: 1; 95 | opacity: 0.7; 96 | 97 | * { 98 | line-height: 1; 99 | } 100 | } 101 | 102 | .kg-bookmark-description { 103 | color: @color-content-title; 104 | font-size: 0.5em; 105 | font-weight: 300; 106 | line-height: 1.2; 107 | } 108 | 109 | .kg-bookmark-thumbnail { 110 | position: relative; 111 | flex-basis: 25rem; 112 | @media(max-width: @mobile-breakpoint) { 113 | max-height: 400px; 114 | overflow: hidden; 115 | } 116 | 117 | img { 118 | width: 100%; 119 | margin-bottom: 0 !important; 120 | object-fit: cover; 121 | vertical-align: bottom; 122 | @media(max-width: @mobile-breakpoint) { 123 | position: absolute; 124 | top: 0; 125 | bottom: 0; 126 | margin: auto; 127 | } 128 | } 129 | } 130 | 131 | .kg-bookmark-icon { 132 | width: 22px !important; 133 | height: 22px !important; 134 | margin-right: 8px !important; 135 | margin-bottom: 0 !important; 136 | vertical-align: bottom; 137 | } 138 | 139 | .kg-bookmark-author:after { 140 | margin: 0 6px; 141 | content: "•"; 142 | } 143 | -------------------------------------------------------------------------------- /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 |