├── .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 | [](https://app.netlify.com/sites/sleepy-shirley-d61a1e/deploys)
4 | 
5 | 
6 | 
7 | 
8 | 
9 | [](https://github.com/toddbirchard/gatsby-ghost-tokyo/issues)
10 | [](https://github.com/toddbirchard/gatsby-ghost-tokyo/stargazers)
11 | [](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 | 
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 | )
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}
}
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 |
45 | >
46 | )
47 | }
48 |
49 | DefaultLayout.propTypes = {
50 | children: PropTypes.node.isRequired,
51 | bodyClass: PropTypes.string,
52 | isHome: PropTypes.bool,
53 | template: PropTypes.string,
54 | data: PropTypes.shape({
55 | allGhostSettings: PropTypes.object.isRequired,
56 | allGhostTag: PropTypes.object.isRequired,
57 | }).isRequired,
58 | }
59 |
60 | const DefaultLayoutSettingsQuery = props => (
61 | }
91 | />
92 | )
93 |
94 | export default DefaultLayoutSettingsQuery
95 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
22 |
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/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 |
--------------------------------------------------------------------------------
/src/components/common/meta/ArticleMeta.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Helmet } from "react-helmet"
3 | import { StaticQuery, graphql } from 'gatsby'
4 | import PropTypes from 'prop-types'
5 | import _ from 'lodash'
6 | import url from 'url'
7 |
8 | import getAuthorProperties from './getAuthorProperties'
9 | import ImageMeta from './ImageMeta'
10 | import config from '../../../utils/siteConfig'
11 |
12 | import { tags as tagsHelper } from '@tryghost/helpers'
13 |
14 | const ArticleMetaGhost = ({ data, settings, canonical }) => {
15 | const ghostPost = data
16 | settings = settings.allGhostSettings.edges[0].node
17 |
18 | const author = getAuthorProperties(ghostPost.primary_author)
19 | const publicTags = _.map(tagsHelper(ghostPost, { visibility: `public`, fn: tag => tag }), `name`)
20 | const primaryTag = publicTags[0] || ``
21 | const shareImage = ghostPost.feature_image ? ghostPost.feature_image : _.get(settings, `cover_image`, null)
22 | const publisherLogo = (settings.logo || config.siteIcon) ? url.resolve(config.siteUrl, (settings.logo || config.siteIcon)) : null
23 |
24 | return (
25 | <>
26 |
27 | {ghostPost.meta_title || ghostPost.title}
28 |
29 |
30 |
31 |
32 |
39 |
46 |
47 |
48 |
49 | {publicTags.map((keyword, i) => ())}
50 |
51 |
58 |
65 |
66 |
67 |
68 | {primaryTag && }
69 | {primaryTag && }
70 | {settings.twitter && }
71 | {settings.twitter && }
72 |
110 |
111 |
112 | >
113 | )
114 | }
115 |
116 | ArticleMetaGhost.propTypes = {
117 | data: PropTypes.shape({
118 | title: PropTypes.string.isRequired,
119 | published_at: PropTypes.string.isRequired,
120 | updated_at: PropTypes.string.isRequired,
121 | meta_title: PropTypes.string,
122 | meta_description: PropTypes.string,
123 | primary_author: PropTypes.object.isRequired,
124 | feature_image: PropTypes.string,
125 | tags: PropTypes.arrayOf(
126 | PropTypes.shape({
127 | name: PropTypes.string,
128 | slug: PropTypes.string,
129 | visibility: PropTypes.string,
130 | })
131 | ),
132 | primaryTag: PropTypes.shape({
133 | name: PropTypes.string,
134 | }),
135 | og_title: PropTypes.string,
136 | og_description: PropTypes.string,
137 | twitter_title: PropTypes.string,
138 | twitter_description: PropTypes.string,
139 | excerpt: PropTypes.string.isRequired,
140 | }).isRequired,
141 | settings: PropTypes.shape({
142 | allGhostSettings: PropTypes.object.isRequired,
143 | }).isRequired,
144 | canonical: PropTypes.string.isRequired,
145 | }
146 |
147 | const ArticleMetaQuery = props => (
148 | }
161 | />
162 | )
163 |
164 | export default ArticleMetaQuery
165 |
--------------------------------------------------------------------------------
/src/components/common/meta/AuthorMeta.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Helmet } from 'react-helmet'
3 | import PropTypes from 'prop-types'
4 | import _ from 'lodash'
5 | import { StaticQuery, graphql } from 'gatsby'
6 |
7 | import ImageMeta from './ImageMeta'
8 | import getAuthorProperties from './getAuthorProperties'
9 | import config from '../../../utils/siteConfig'
10 |
11 | const AuthorMeta = ({ data, settings, canonical }) => {
12 | settings = settings.allGhostSettings.edges[0].node
13 |
14 | const author = getAuthorProperties(data)
15 | const shareImage = author.image || _.get(settings, `cover_image`, null)
16 | const title = `${data.name} - ${settings.title}`
17 | const description = data.bio || config.siteDescriptionMeta || settings.description
18 |
19 | return (
20 | <>
21 |
22 | {title}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {settings.twitter && }
34 | {settings.twitter && }
35 |
55 |
56 |
57 | >
58 | )
59 | }
60 |
61 | AuthorMeta.propTypes = {
62 | data: PropTypes.shape({
63 | name: PropTypes.string,
64 | bio: PropTypes.string,
65 | profile_image: PropTypes.string,
66 | website: PropTypes.string,
67 | twitter: PropTypes.string,
68 | }).isRequired,
69 | settings: PropTypes.shape({
70 | allGhostSettings: PropTypes.object.isRequired,
71 | }).isRequired,
72 | canonical: PropTypes.string.isRequired,
73 | }
74 |
75 | const AuthorMetaQuery = props => (
76 | }
89 | />
90 | )
91 |
92 | export default AuthorMetaQuery
93 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/common/meta/MetaData.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { StaticQuery, graphql } from 'gatsby'
4 | import url from 'url'
5 |
6 | import config from '../../../utils/siteConfig'
7 | import ArticleMeta from './ArticleMeta'
8 | import WebsiteMeta from './WebsiteMeta'
9 | import AuthorMeta from './AuthorMeta'
10 |
11 | /**
12 | * MetaData will generate all relevant meta data information incl.
13 | * JSON-LD (schema.org), Open Graph (Facebook) and Twitter properties.
14 | *
15 | */
16 | const MetaData = ({
17 | data,
18 | settings,
19 | title,
20 | description,
21 | image,
22 | location,
23 | pageContext
24 | }) => {
25 | const canonical = url.resolve(config.siteUrl, location.pathname)
26 | const { ghostPost, ghostTag, ghostAuthor, ghostPage } = data
27 | settings = settings.allGhostSettings.edges[0].node
28 |
29 | if (ghostPost) {
30 | return (
31 |
35 | )
36 | } else if (ghostTag) {
37 | return (
38 |
44 | )
45 | } else if (ghostAuthor) {
46 | return (
47 |
52 | )
53 | } else if (ghostPage) {
54 | return (
55 |
61 | )
62 | } else {
63 | title = title || config.siteTitleMeta || settings.title
64 | description = description || config.siteDescriptionMeta || settings.description
65 | image = image || settings.cover_image || null
66 |
67 | image = image ? url.resolve(config.siteUrl, image) : null
68 |
69 | return (
70 |
79 | )
80 | }
81 | }
82 |
83 | MetaData.defaultProps = {
84 | data: {},
85 | }
86 |
87 | MetaData.propTypes = {
88 | data: PropTypes.shape({
89 | ghostPost: PropTypes.object,
90 | ghostTag: PropTypes.object,
91 | ghostAuthor: PropTypes.object,
92 | ghostPage: PropTypes.object,
93 | }).isRequired,
94 | settings: PropTypes.shape({
95 | allGhostSettings: PropTypes.object.isRequired,
96 | }).isRequired,
97 | location: PropTypes.shape({
98 | pathname: PropTypes.string.isRequired,
99 | }).isRequired,
100 | title: PropTypes.string,
101 | description: PropTypes.string,
102 | image: PropTypes.string,
103 | }
104 |
105 | const MetaDataQuery = props => (
106 | }
120 | />
121 | )
122 |
123 | export default MetaDataQuery
124 |
--------------------------------------------------------------------------------
/src/components/common/meta/WebsiteMeta.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Helmet } from 'react-helmet'
3 | import PropTypes from 'prop-types'
4 | import _ from 'lodash'
5 | import { StaticQuery, graphql } from 'gatsby'
6 | import url from 'url'
7 | import ImageMeta from './ImageMeta'
8 | import config from '../../../utils/siteConfig'
9 |
10 | const WebsiteMeta = ({ data, settings, canonical, title, description, image, pageContext, type }) => {
11 | settings = settings.ghostSettings
12 | const facebookPageID = process.env.FACEBOOK_PAGE_ID
13 | const facebookAppID = process.env.FACEBOOK_APP_ID
14 | const googleVerificationID = process.env.GOOGLE_VERIFICATION_ID
15 | const previousPagePath = pageContext ? pageContext.previousPagePath : null
16 | const nextPagePath = pageContext ? pageContext.nextPagePath : null
17 |
18 | const publisherLogo = settings.logo || config.images.siteIcon
19 | let shareImage = image || data.feature_image || _.get(settings, `cover_image`, null)
20 |
21 | shareImage = shareImage ? settings.images.shareImage : null
22 |
23 | description = description || data.meta_description || data.description || config.siteDescriptionMeta || settings.description
24 | title = `${title || data.meta_title || data.name || data.title}`
25 |
26 | return (
27 | <>
28 |
29 |
30 | {canonical === config.siteUrl + `/`
31 | ? title
32 | : title + ` - ${settings.title}`
33 | }
34 |
35 |
36 |
37 |
38 | {previousPagePath ? : null}
39 | {nextPagePath ? : null}
40 | {googleVerificationID ? : null}
41 |
42 | {/* Facebook */}
43 |
44 |
45 |
46 |
47 |
48 | {facebookPageID ? : null}
49 | {facebookAppID ? : null}
50 |
51 | {/* Twitter */}
52 |
53 |
54 |
55 | {settings.twitter ? : null}
56 | {config.creator.twitter ? : null}
57 |
58 |
87 |
88 |
89 | >
90 | )
91 | }
92 |
93 | WebsiteMeta.propTypes = {
94 | data: PropTypes.shape({
95 | title: PropTypes.string,
96 | feature_image: PropTypes.string,
97 | description: PropTypes.string,
98 | bio: PropTypes.string,
99 | profile_image: PropTypes.string,
100 | meta_description: PropTypes.string,
101 | name: PropTypes.string,
102 | meta_title: PropTypes.string,
103 | }).isRequired,
104 | settings: PropTypes.shape({
105 | ghostSettings: PropTypes.object.isRequired,
106 | twitter: PropTypes.object,
107 | title: PropTypes.string,
108 | logo: PropTypes.string,
109 | images: PropTypes.string,
110 | description: PropTypes.string,
111 | }).isRequired,
112 | canonical: PropTypes.string.isRequired,
113 | title: PropTypes.string,
114 | description: PropTypes.string,
115 | image: PropTypes.string,
116 | pageContext: PropTypes.object,
117 | type: PropTypes.oneOf([`WebSite`, `Series`]).isRequired,
118 | }
119 |
120 | const WebsiteMetaQuery = props => (
121 | }
130 | />
131 | )
132 |
133 | export default WebsiteMetaQuery
134 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/common/meta/index.js:
--------------------------------------------------------------------------------
1 | export { default as MetaData } from './MetaData'
2 |
--------------------------------------------------------------------------------
/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 | )
17 |
18 | export default Hamburger
19 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 &&

}
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/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 |
--------------------------------------------------------------------------------
/src/components/common/posts/index.js:
--------------------------------------------------------------------------------
1 | export { default as RecentPosts } from './RecentPosts'
2 | export { default as PostAuthor } from './PostAuthor'
3 |
--------------------------------------------------------------------------------
/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}
}
14 |
15 |
{site.description}
16 |
17 | >
18 | )
19 | }
20 |
21 | export default AuthorWidget
22 |
--------------------------------------------------------------------------------
/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/components/sidebar/TwitterWidget.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { StaticQuery, graphql } from 'gatsby'
4 | import { FaRetweet, FaHeartbeat, FaCalendar } from 'react-icons/fa'
5 |
6 | const TwitterWidget = ({ data }) => {
7 | const tweets = data.tweets.edges
8 | const twitterProfile = data.twitterProfile.user
9 | const twitterProfileURL = `https://twitter.com/${twitterProfile.screen_name}/`
10 |
11 | return (
12 | <>
13 |
14 |
15 |

16 |
20 |
21 |
22 | {tweets.map(({ node }) => (
23 |
24 |
{node.full_text.split(`#`)[0].split(`http`)[0]}
25 | {node.entities.urls.map(({ display_url, expanded_url }) => (
26 |
{ display_url }
31 |
32 | ))}
33 |
34 |
35 |
36 | {node.retweet_count}
37 |
38 |
39 | {node.favorite_count}
40 |
41 |
42 | {node.created_at.split(` `, 3).join(` `)}
43 |
44 |
45 |
46 |
47 | ))}
48 |
49 |
50 | >
51 | )
52 | }
53 |
54 | TwitterWidget.propTypes = {
55 | data: PropTypes.shape({
56 | tweets: PropTypes.shape({
57 | edges: PropTypes.arrayOf(
58 | PropTypes.shape({
59 | full_text: PropTypes.string,
60 | favorite_count: PropTypes.number,
61 | retweet_count: PropTypes.number,
62 | created_at: PropTypes.string,
63 | id: PropTypes.string,
64 | retweeted: PropTypes.bool,
65 | retweeted_status: PropTypes.object,
66 | in_reply_to_screen_name: PropTypes.string,
67 | user: PropTypes.shape({
68 | name: PropTypes.string.isRequired,
69 | url: PropTypes.string.isRequired,
70 | profile_image_url_https: PropTypes.string.isRequired,
71 | screen_name: PropTypes.string.isRequired,
72 | }),
73 | entities: PropTypes.shape({
74 | urls: PropTypes.arrayOf(
75 | PropTypes.shape({
76 | url: PropTypes.string,
77 | }),
78 | ),
79 | }),
80 | }).isRequired,
81 | ),
82 | }),
83 | twitterProfile: PropTypes.shape({
84 | user: PropTypes.shape({
85 | profile_image_url_https: PropTypes.string,
86 | name: PropTypes.string.isRequired,
87 | url: PropTypes.string.isRequired,
88 | display_url: PropTypes.string,
89 | screen_name: PropTypes.string.isRequired,
90 | followers_count: PropTypes.number.isRequired,
91 | }).isRequired,
92 | }),
93 | }),
94 | }
95 |
96 | const TwitterQuery = props => (
97 | }
147 | />
148 | )
149 |
150 | export default TwitterQuery
151 |
--------------------------------------------------------------------------------
/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/images/ghost-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/src/images/ghost-icon.png
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/global.less:
--------------------------------------------------------------------------------
1 | /* App.css
2 | /* ---------------------------------------------------------- */
3 | a,
4 | abbr,
5 | acronym,
6 | address,
7 | applet,
8 | article,
9 | aside,
10 | audio,
11 | big,
12 | blockquote,
13 | body,
14 | canvas,
15 | caption,
16 | cite,
17 | code,
18 | dd,
19 | del,
20 | details,
21 | dfn,
22 | div,
23 | dl,
24 | dt,
25 | em,
26 | embed,
27 | fieldset,
28 | figcaption,
29 | figure,
30 | footer,
31 | form,
32 | h1,
33 | h2,
34 | h3,
35 | h4,
36 | h5,
37 | h6,
38 | header,
39 | hgroup,
40 | html,
41 | iframe,
42 | img,
43 | ins,
44 | kbd,
45 | label,
46 | legend,
47 | li,
48 | mark,
49 | menu,
50 | nav,
51 | object,
52 | ol,
53 | output,
54 | p,
55 | pre,
56 | q,
57 | ruby,
58 | s,
59 | samp,
60 | section,
61 | small,
62 | span,
63 | strike,
64 | strong,
65 | sub,
66 | summary,
67 | sup,
68 | table,
69 | tbody,
70 | td,
71 | tfoot,
72 | th,
73 | thead,
74 | time,
75 | tr,
76 | tt,
77 | ul,
78 | var,
79 | video {
80 | margin: 0;
81 | padding: 0;
82 | border: 0;
83 | font: inherit;
84 | font-size: 100%;
85 | vertical-align: baseline;
86 | }
87 |
88 | body {
89 | background: #f8f8f8;
90 | font-family: @font-body;
91 | line-height: 1;
92 | }
93 |
94 | ol,
95 | ul {
96 | list-style: none;
97 | }
98 |
99 | blockquote,
100 | q {
101 | quotes: none;
102 | }
103 |
104 | blockquote:after,
105 | blockquote:before,
106 | q:after,
107 | q:before {
108 | content: "";
109 | content: none;
110 | }
111 |
112 | table {
113 | border-collapse: collapse;
114 | border-spacing: 0;
115 | }
116 |
117 | img {
118 | max-width: 100%;
119 | }
120 |
121 | html {
122 | -ms-text-size-adjust: 100%;
123 | -webkit-text-size-adjust: 100%;
124 | box-sizing: border-box;
125 | font-family: 'FFMarkWebProBook', Helvetica, Sans-Serif;
126 | }
127 |
128 | *,
129 | *:after,
130 | *:before {
131 | box-sizing: inherit;
132 | }
133 |
134 | a {
135 | background-color: transparent;
136 | }
137 |
138 | a:active,
139 | a:hover {
140 | outline: 0;
141 | }
142 |
143 | b,
144 | strong {
145 | font-weight: bold;
146 | }
147 |
148 | dfn,
149 | em,
150 | i {
151 | font-style: italic;
152 | }
153 |
154 | h1 {
155 | margin: 0.67em 0;
156 | font-family: @font-title;
157 | font-size: 2em;
158 | }
159 |
160 | small {
161 | font-size: 80%;
162 | }
163 |
164 | sub,
165 | sup {
166 | position: relative;
167 | font-size: 75%;
168 | line-height: 0;
169 | vertical-align: baseline;
170 | }
171 |
172 | sup {
173 | top: -0.5em;
174 | }
175 |
176 | sub {
177 | bottom: -0.25em;
178 | }
179 |
180 | img {
181 | border: 0;
182 | }
183 |
184 | svg:not(:root) {
185 | overflow: hidden;
186 | }
187 |
188 | mark {
189 | background-color: #fdffb6;
190 | }
191 |
192 | code,
193 | kbd,
194 | pre,
195 | samp {
196 | font-family: monospace, monospace;
197 | font-size: 1em;
198 | }
199 |
200 | button,
201 | input,
202 | optgroup,
203 | select,
204 | textarea {
205 | margin: 0;
206 | color: inherit;
207 | font: inherit;
208 | }
209 |
210 | button {
211 | overflow: visible;
212 | border: none;
213 | }
214 |
215 | button,
216 | select {
217 | text-transform: none;
218 | }
219 |
220 | button,
221 | html input[type="button"],
222 | input[type="reset"],
223 | input[type="submit"] {
224 | -webkit-appearance: button;
225 | cursor: pointer;
226 | }
227 |
228 | button[disabled],
229 | html input[disabled] {
230 | cursor: default;
231 | }
232 |
233 | button::-moz-focus-inner,
234 | input::-moz-focus-inner {
235 | padding: 0;
236 | border: 0;
237 | }
238 |
239 | input {
240 | line-height: normal;
241 | }
242 |
243 | input:focus {
244 | outline: none;
245 | }
246 |
247 | input[type="checkbox"],
248 | input[type="radio"] {
249 | box-sizing: border-box;
250 | padding: 0;
251 | }
252 |
253 | input[type="number"]::-webkit-inner-spin-button,
254 | input[type="number"]::-webkit-outer-spin-button {
255 | height: auto;
256 | }
257 |
258 | input[type="search"] {
259 | -webkit-appearance: textfield;
260 | box-sizing: content-box;
261 | }
262 |
263 | input[type="search"]::-webkit-search-cancel-button,
264 | input[type="search"]::-webkit-search-decoration {
265 | -webkit-appearance: none;
266 | }
267 |
268 | legend {
269 | padding: 0;
270 | border: 0;
271 | }
272 |
273 | textarea {
274 | overflow: auto;
275 | }
276 |
277 | table {
278 | border-collapse: collapse;
279 | border-spacing: 0;
280 | }
281 |
282 | td,
283 | th {
284 | padding: 0;
285 | }
286 | /* Defaults
287 | /* ---------------------------------------------------------- */
288 | html {
289 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
290 | overflow-x: hidden;
291 | overflow-y: scroll;
292 | background: @color-base;
293 | font-size: 62.5%;
294 | }
295 |
296 | body {
297 | -webkit-font-smoothing: antialiased;
298 | -moz-osx-font-smoothing: grayscale;
299 | -moz-font-feature-settings: "liga";
300 | overflow-x: hidden;
301 | background: #f8f8f8;
302 | color: #3c484e;
303 | font-family: @font-body;
304 | font-size: 1.5rem;
305 | font-style: normal;
306 | font-weight: 400;
307 | letter-spacing: 0;
308 | line-height: 1.6em;
309 | text-rendering: optimizeLegibility;
310 | }
311 |
312 | ::selection {
313 | background: #cbeafb;
314 | text-shadow: none;
315 | }
316 |
317 | hr {
318 | display: block;
319 | position: relative;
320 | width: 100%;
321 | height: 1px;
322 | margin: 1.8em 0 2.4em;
323 | padding: 0;
324 | border: 0;
325 | border-top: 1px solid #e3e9ed;
326 | }
327 |
328 | audio,
329 | canvas,
330 | iframe,
331 | img,
332 | svg,
333 | video {
334 | vertical-align: middle;
335 | }
336 |
337 | fieldset {
338 | margin: 0;
339 | padding: 0;
340 | border: 0;
341 | }
342 |
343 | textarea {
344 | resize: vertical;
345 | }
346 |
347 | blockquote,
348 | dl,
349 | ol,
350 | p,
351 | ul {
352 | margin: 0 0 1.5em;
353 | }
354 |
355 | ol,
356 | ul {
357 | padding-right: 1.5em;
358 | padding-left: 1.3em;
359 | }
360 |
361 | ol ol,
362 | ol ul,
363 | ul ol,
364 | ul ul {
365 | margin: 0.5em 0 1em;
366 | }
367 |
368 | ul {
369 | list-style: disc;
370 | }
371 |
372 | ol {
373 | list-style: decimal;
374 | }
375 |
376 | ol,
377 | ul {
378 | max-width: 100%;
379 | }
380 |
381 | li {
382 | margin: 0.5em 0;
383 | padding-left: 0.3em;
384 | line-height: 1.6em;
385 | }
386 |
387 | dt {
388 | width: 120px;
389 | margin: 0 20px 0 0;
390 | float: left;
391 | font-weight: 500;
392 | text-align: right;
393 | }
394 |
395 | dd {
396 | margin: 0 0 5px;
397 | text-align: left;
398 | }
399 |
400 | blockquote {
401 | width: 90%;
402 | margin: 0.3em auto 1.8em;
403 | padding: 0 1.6em;
404 | border-left: #a67585 0.5em solid;
405 | color: #a67585;
406 | font-style: italic;
407 | font-weight: bold;
408 |
409 | p {
410 | margin: 0.8em 0;
411 | font-size: 1.2em;
412 | font-weight: 300;
413 | }
414 |
415 | small {
416 | display: inline-block;
417 | margin: 0.8em 0 0.8em 1.5em;
418 | font-size: 0.9em;
419 | opacity: 0.8;
420 |
421 | &:before {
422 | content: "\2014 \00A0";
423 | }
424 | }
425 |
426 | cite {
427 | font-weight: bold;
428 |
429 | a {
430 | font-weight: normal;
431 | }
432 | }
433 | }
434 |
435 | a {
436 | color: @color-primary;
437 | font-weight: bold;
438 | text-decoration: none;
439 |
440 | &:hover {
441 | text-decoration: underline;
442 | }
443 | }
444 |
445 | h1,
446 | h2,
447 | h3,
448 | h4,
449 | h5,
450 | h6 {
451 | margin-top: 0;
452 | color: @color-content-title;
453 | font-family: @font-title;
454 | font-weight: 700;
455 | line-height: 1.1;
456 | text-rendering: optimizeLegibility;
457 | }
458 |
459 | h1 {
460 | margin: 0 0 0.5em;
461 | font-size: 1.9em;
462 | font-weight: 700;
463 | @media (max-width: @mobile-breakpoint) {
464 | font-size: 1.3em;
465 | }
466 | }
467 |
468 | h2 {
469 | margin: 1.5em 0 0.5em;
470 | font-size: 2rem;
471 | @media (max-width: 500px) {
472 | font-size: 1.8rem;
473 | }
474 | }
475 |
476 | h3 {
477 | margin: 1.5em 0 0.5em;
478 | font-size: 1.8rem;
479 | font-weight: 500;
480 | @media (max-width: 500px) {
481 | font-size: 1.7rem;
482 | }
483 | }
484 |
485 | h4 {
486 | margin: 1.5em 0 0.5em;
487 | font-size: 1.6rem;
488 | font-weight: 500;
489 | }
490 |
491 | h5 {
492 | margin: 1.5em 0 0.5em;
493 | font-size: 1.4rem;
494 | font-weight: 500;
495 | }
496 |
497 | h6 {
498 | margin: 1.5em 0 0.5em;
499 | font-size: 1.4rem;
500 | font-weight: 500;
501 | }
502 |
503 | .mobile-logo {
504 | display: none;
505 | width: 100%;
506 | margin: 20px auto 0;
507 | transition: 0.2s all ease-out;
508 | @media(max-width: @tablet-breakpoint) {
509 | display: block;
510 | }
511 |
512 | &:hover {
513 | opacity: 0.7;
514 | }
515 |
516 | img {
517 | width: 100%;
518 | }
519 | }
520 |
521 | button {
522 | outline-color: transparent;
523 | outline-style: none;
524 | }
525 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/src/styles/mixins.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/src/styles/mixins.less
--------------------------------------------------------------------------------
/src/styles/navigation.less:
--------------------------------------------------------------------------------
1 | /* Navigation
2 | /* ---------------------------------------------------------- */
3 | .home-template .navigation,
4 | .tag-template .navigation {
5 | width: 100%;
6 | max-width: 1020px;
7 | margin: 30px auto;
8 | text-align: right;
9 | @media(max-width: @tablet-breakpoint) {
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 | }
14 | }
15 |
16 | .home-template .navigation .nav-logo,
17 | .tag-template .navigation .nav-logo {
18 | display: none;
19 | }
20 |
21 | .post-template .navigation {
22 | display: flex;
23 | position: absolute;
24 | right: 0;
25 | left: 0;
26 | align-items: center;
27 | justify-content: space-between;
28 | width: 100%;
29 | max-width: @tablet-breakpoint;
30 | margin: 20px auto;
31 | @media(max-width: @tablet-breakpoint) {
32 | max-width: 85%;
33 | }
34 | }
35 |
36 | .post-template .navigation .nav-logo {
37 | display: block;
38 | width: 50px;
39 | height: 50px;
40 | transition: all 0.2s ease-out;
41 |
42 | &:hover {
43 | cursor: pointer;
44 | opacity: 0.7;
45 | }
46 | }
47 |
48 | .post-template .navigation .site-nav-item {
49 | color: #fff;
50 | font-size: 1.1em;
51 | font-weight: 600;
52 | text-shadow: -1px -1px 0 #555, 1px -1px 0 #555, -1px 1px 0 #555, 1px 1px 0 #555;
53 | opacity: 1;
54 |
55 | &:hover {
56 | color: @color-primary;
57 | }
58 | }
59 |
60 | .post-social {
61 | display: flex;
62 | align-items: center;
63 | justify-content: space-between;
64 | margin: 20px 0;
65 |
66 | svg {
67 | color: #c3c3c3;
68 | font-size: 2em;
69 | }
70 | }
71 | /* Header
72 | /* ---------------------------------------------------------- */
73 | .site-head {
74 | padding-top: 20px;
75 | padding-bottom: 20px;
76 | background: @color-base;
77 | background-position: center;
78 | background-size: cover;
79 | color: #fff;
80 | }
81 |
82 | .navigation-links .site-nav-item {
83 | display: inline-block;
84 | margin-left: 30px;
85 | padding: 0;
86 | color: #fff;
87 | color: #606a6e;
88 | font-size: 1.1em;
89 | font-family: @font-title;
90 | font-weight: 400;
91 | line-height: 1;
92 | text-transform: uppercase;
93 | opacity: 1;
94 | opacity: 0.8;
95 | transition: 0.2s all ease-out;
96 |
97 | &:hover {
98 | color: @color-primary;
99 | text-decoration: none;
100 | opacity: 1;
101 | }
102 | }
103 |
104 | .site-nav-icon {
105 | height: 15px;
106 | margin: -5px 0 0;
107 | }
108 |
109 | .site-logo {
110 | margin-bottom: 10px;
111 | }
112 |
113 | .site-nav {
114 | display: flex;
115 | align-items: center;
116 | justify-content: space-between;
117 | margin: 15px 0 0;
118 | }
119 |
120 | .site-nav-left {
121 | margin: 0 20px 0 -10px;
122 | }
123 | /* TODO: Clean this up
124 | /* ---------------------------------------------------------- */
125 | @media(max-width: @tablet-breakpoint) {
126 | .home-template .navigation-links {
127 | display: flex;
128 | align-items: center;
129 | justify-content: space-between;
130 | width: 100%;
131 | }
132 | }
133 | @media(max-width: @tablet-breakpoint) {
134 | .home-template .site-nav-item {
135 | margin: 0;
136 | }
137 |
138 | .post-template .navigation-links .site-nav-item {
139 | margin-left: 10px;
140 | font-size: 0.9em !important;
141 | }
142 | }
143 | @media(max-width: @mobile-breakpoint) {
144 | .post-template .navigation-links {
145 | display: none;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/styles/page.less:
--------------------------------------------------------------------------------
1 | .page-template .content-title {
2 | padding-bottom: 20px;
3 | border-bottom: 1px solid #d8d8d8;
4 | }
5 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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/styles/sidebar.less:
--------------------------------------------------------------------------------
1 | /* Sidebar
2 | /* ---------------------------------------------------------- */
3 | .widget {
4 | margin: 50px 0;
5 |
6 | &:first-of-type {
7 | margin: 0 0 50px;
8 | }
9 |
10 | &:last-of-type {
11 | margin: 50px 0 0;
12 | }
13 |
14 | &.twitter {
15 | padding: 20px;
16 | overflow: hidden;
17 |
18 | .twitter-header {
19 | display: flex;
20 | align-items: center;
21 | margin-bottom: 30px;
22 |
23 | .twitter-avatar {
24 | height: 40px;
25 | margin-right: 15px;
26 | overflow: hidden;
27 | }
28 |
29 | .twitter-name {
30 | color: #4b5776;
31 | font-size: 1.1em;
32 | font-weight: 600;
33 | line-height: 1;
34 | }
35 |
36 | .twitter-user {
37 | color: #516b83;
38 | font-size: 1em;
39 | font-weight: 500;
40 | font-weight: 400;
41 | line-height: 1;
42 | }
43 | }
44 |
45 | .tweet {
46 | padding: 35px 0;
47 | border-bottom: 1px solid #e7e7e7;
48 | font-family: @font-body;
49 |
50 | &:last-of-type {
51 | padding: 35px 0 0;
52 | border-bottom: 0;
53 | }
54 |
55 | &:first-of-type {
56 | padding: 0 0 35px;
57 | }
58 |
59 | .tweet-footer {
60 | display: flex;
61 | margin-top: 15px;
62 |
63 | * {
64 | font-size: 0.9em;
65 | font-weight: 600;
66 | }
67 |
68 | .meta-item {
69 | margin-right: 15px;
70 | }
71 |
72 | .date {
73 | color: #8b97a5;
74 | }
75 |
76 | .retweets {
77 | color: #59d39e;
78 | }
79 |
80 | .favorites {
81 | color: #d8819c;
82 | }
83 | }
84 |
85 | .tweet-link {
86 | display: block;
87 | overflow: hidden;
88 | color: @color-primary;
89 | font-family: @font-body;
90 | font-size: 0.9em;
91 | font-weight: 600;
92 | line-height: 1;
93 | text-overflow: ellipsis;
94 | white-space: nowrap;
95 | }
96 |
97 | .tweet-hastags {
98 | width: 100%;
99 | margin: 15px 0;
100 |
101 | .hashtag {
102 | display: inline-block;
103 | margin: 0 8px 5px 0;
104 | color: #6a849f;
105 | font-size: 0.9em;
106 | }
107 | }
108 |
109 | .tweet-content {
110 | margin: 0 0 15px;
111 | font-size: 1.0em;
112 | line-height: 1.3;
113 | }
114 | }
115 | }
116 | }
117 |
118 | .widget.about {
119 | .about-logo-link {
120 | transition: 0.2s all ease-out;
121 |
122 | &:hover {
123 | opacity: 0.7;
124 | }
125 | }
126 |
127 | .description {
128 | margin: 10px 0 1.5em;
129 | font-size: 1.1em;
130 | line-height: 1.4em;
131 | }
132 | }
133 |
134 | .widget.social {
135 | display: flex;
136 | justify-content: space-between;
137 | margin: 50px 0;
138 | width: 95%;
139 |
140 | a {
141 | fill: @color-secondary;
142 | opacity: 0.7;
143 | transition: all 0.2s ease-out;
144 |
145 | &:hover {
146 | fill: @color-secondary;
147 | opacity: 1;
148 | }
149 | }
150 |
151 | svg {
152 | color: @color-secondary !important;
153 | font-size: 1.7em;
154 | }
155 | }
156 |
157 | .widget.tags .tag {
158 | display: inline-block;
159 | margin: 0 8px 8px 0;
160 | padding: 8px 10px;
161 | border: 1px solid #ececec;
162 | border-radius: 4px;
163 | background: #fff;
164 | color: #797979;
165 | font-size: 0.9em;
166 | line-height: 1;
167 | text-decoration: none;
168 | transition: 0.2s all ease-out;
169 |
170 | &:hover {
171 | border-color: #b15d5d;
172 | background: #b15d5d;
173 | color: white;
174 | }
175 | }
176 | @media(max-width: @tablet-breakpoint) {
177 | .sidebar,
178 | .widget {
179 | display: none !important;
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/src/templates/post.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link, graphql } from 'gatsby'
4 | import { Helmet } from 'react-helmet'
5 | import { readingTime as readingTimeHelper } from '@tryghost/helpers'
6 | import { Tags } from '@tryghost/helpers-gatsby'
7 | import { FaUserEdit, FaEye, FaTag } from 'react-icons/fa'
8 | import { Layout } from '../components/common'
9 | import { MetaData } from '../components/common/meta'
10 | import { RecentPosts, PostAuthor } from '../components/common/posts'
11 |
12 | import '../styles/post/index.less'
13 |
14 | /**
15 | * Single post view (/:slug)
16 | *
17 | * This file renders a single post and loads all the content.
18 | *
19 | */
20 |
21 | const Post = ({ data, location }) => {
22 | const post = data.ghostPost
23 | const readingTime = readingTimeHelper(post)
24 | const authorUrl = post.primary_author.slug ? `author/${post.primary_author.slug}` : null
25 |
26 | return (
27 | <>
28 |
33 |
34 |
35 |
36 |
37 |
38 | { post.feature_image ?
39 |
40 |
41 | : null }
42 |
43 | {post.title}
44 |
45 |
{post.primary_author.name}
46 |
{post.tags && }
47 |
{readingTime}
48 |
49 |
50 | {/* The main post content */ }
51 |
55 |
56 |
57 |
58 |
59 | {post.tags.map(({ name, slug }) => (
60 | { name }
61 | ))}
62 |
63 |
64 |
65 |
66 |
67 | >
68 | )
69 | }
70 |
71 | Post.propTypes = {
72 | data: PropTypes.shape({
73 | ghostPost: PropTypes.shape({
74 | title: PropTypes.string.isRequired,
75 | html: PropTypes.string.isRequired,
76 | feature_image: PropTypes.string,
77 | tags: PropTypes.shape({
78 | name: PropTypes.string.isRequired,
79 | url: PropTypes.string.isRequired,
80 | slug: PropTypes.string.isRequired,
81 | }).isRequired,
82 | primary_author: PropTypes.object.isRequired,
83 | codeinjection_styles: PropTypes.string,
84 | }).isRequired,
85 | }).isRequired,
86 | location: PropTypes.object.isRequired,
87 | }
88 |
89 | export default Post
90 |
91 | export const postQuery = graphql`
92 | query($slug: String!) {
93 | ghostPost(slug: { eq: $slug }) {
94 | ...GhostPostFields
95 | primary_author {
96 | name
97 | url
98 | bio
99 | website
100 | twitter
101 | }
102 | }
103 | }
104 | `
105 |
--------------------------------------------------------------------------------
/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/utils/fragments.js:
--------------------------------------------------------------------------------
1 | import { graphql } from 'gatsby'
2 |
3 | // Used for tag archive pages
4 | export const ghostTagFields = graphql`
5 | fragment GhostTagFields on GhostTag {
6 | slug
7 | name
8 | visibility
9 | feature_image
10 | description
11 | meta_title
12 | meta_description
13 | accent_color
14 | }
15 | `
16 |
17 | // Used for author pages
18 | export const ghostAuthorFields = graphql`
19 | fragment GhostAuthorFields on GhostAuthor {
20 | slug
21 | name
22 | bio
23 | cover_image
24 | profile_image
25 | location
26 | website
27 | twitter
28 | facebook
29 | postCount
30 | count {
31 | posts
32 | }
33 | }
34 | `
35 |
36 | // Used for single posts
37 | export const ghostPostFields = graphql`
38 | fragment GhostPostFields on GhostPost {
39 | # Main fields
40 | id
41 | ghostId
42 | title
43 | slug
44 | featured
45 | feature_image
46 | excerpt
47 | custom_excerpt
48 |
49 | # Dates formatted
50 | created_at_pretty: created_at(formatString: "DD MMMM, YYYY")
51 | published_at_pretty: published_at(formatString: "MMMM DD")
52 | updated_at_pretty: updated_at(formatString: "DD MMMM, YYYY")
53 |
54 | # Dates unformatted
55 | created_at
56 | published_at
57 | updated_at
58 |
59 | # SEO
60 | meta_title
61 | meta_description
62 | og_description
63 | og_image
64 | og_title
65 | twitter_description
66 | twitter_image
67 | twitter_title
68 |
69 | # Authors
70 | authors {
71 | name
72 | slug
73 | bio
74 | profile_image
75 | twitter
76 | website
77 | }
78 | primary_author {
79 | name
80 | slug
81 | bio
82 | profile_image
83 | twitter
84 | facebook
85 | website
86 | location
87 | postCount
88 | }
89 |
90 | # Tags
91 | primary_tag {
92 | name
93 | slug
94 | description
95 | feature_image
96 | meta_description
97 | meta_title
98 | visibility
99 | accent_color
100 | }
101 | tags {
102 | name
103 | slug
104 | description
105 | feature_image
106 | meta_description
107 | meta_title
108 | visibility
109 | }
110 |
111 | # Content
112 | plaintext
113 | html
114 |
115 | # Additional fields
116 | url
117 | uuid
118 | comment_id
119 | }
120 | `
121 |
122 | // Used for single pages
123 | export const ghostPageFields = graphql`
124 | fragment GhostPageFields on GhostPage {
125 | # Main fields
126 | title
127 | slug
128 | featured
129 | feature_image
130 | excerpt
131 | custom_excerpt
132 |
133 | # Dates formatted
134 | created_at_pretty: created_at(formatString: "DD MMMM, YYYY")
135 | published_at_pretty: published_at(formatString: "DD MMMM, YYYY")
136 | updated_at_pretty: updated_at(formatString: "DD MMMM, YYYY")
137 |
138 | # Dates unformatted
139 | created_at
140 | published_at
141 | updated_at
142 |
143 | # SEO
144 | meta_title
145 | meta_description
146 | og_description
147 | og_image
148 | og_title
149 | twitter_description
150 | twitter_image
151 | twitter_title
152 |
153 | # Content
154 | plaintext
155 | html
156 |
157 | # Tags
158 | primary_tag {
159 | name
160 | slug
161 | description
162 | feature_image
163 | meta_description
164 | meta_title
165 | visibility
166 | }
167 | tags {
168 | name
169 | slug
170 | visibility
171 | }
172 |
173 | # Additional fields
174 | url
175 | uuid
176 | comment_id
177 | }`
178 |
179 | // Used for settings
180 | export const ghostSettingsFields = graphql`
181 | fragment GhostSettingsFields on GhostSettings {
182 | title
183 | description
184 | logo
185 | icon
186 | cover_image
187 | facebook
188 | twitter
189 | lang
190 | timezone
191 | navigation {
192 | label
193 | url
194 | }
195 | }`
196 |
--------------------------------------------------------------------------------
/src/utils/rss/generate-feed.js:
--------------------------------------------------------------------------------
1 | const cheerio = require(`cheerio`)
2 | const tagsHelper = require(`@tryghost/helpers`).tags
3 | const _ = require(`lodash`)
4 |
5 | const generateItem = function generateItem(siteUrl, post) {
6 | const itemUrl = siteUrl + `/` + post.url
7 | const html = post.html
8 | const htmlContent = cheerio.load(html, { decodeEntities: false, xmlMode: true })
9 | const item = {
10 | title: post.title,
11 | description: post.excerpt,
12 | guid: post.id,
13 | url: itemUrl,
14 | date: post.published_at,
15 | categories: _.map(tagsHelper(post, { visibility: `public`, fn: tag => tag }), `name`),
16 | author: post.primary_author ? post.primary_author.name : null,
17 | custom_elements: [],
18 | }
19 | let imageUrl
20 |
21 | if (post.feature_image) {
22 | imageUrl = post.feature_image
23 |
24 | // Add a media content tag
25 | item.custom_elements.push({
26 | 'media:content': {
27 | _attr: {
28 | url: imageUrl,
29 | medium: `image`,
30 | },
31 | },
32 | })
33 |
34 | // Also add the image to the content, because not all readers support media:content
35 | htmlContent(`p`).first().before(`
`)
36 | htmlContent(`img`).attr(`alt`, post.title)
37 | }
38 |
39 | item.custom_elements.push({
40 | 'content:encoded': {
41 | _cdata: htmlContent.html(),
42 | },
43 | })
44 | return item
45 | }
46 |
47 | const generateRSSFeed = function generateRSSFeed(siteConfig) {
48 | return {
49 | serialize: ({ query: { allGhostPost } }) => allGhostPost.edges.map(edge => Object.assign({}, generateItem(siteConfig.siteUrl, edge.node))),
50 | setup: ({ query: { allGhostSettings } }) => {
51 | const siteTitle = allGhostSettings.edges[0].node.title || `No Title`
52 | const siteDescription = allGhostSettings.edges[0].node.description || `No Description`
53 | const feed = {
54 | title: siteTitle,
55 | description: siteDescription,
56 | // generator: `Ghost ` + data.safeVersion,
57 | generator: `Ghost 2.9`,
58 | feed_url: `${siteConfig.siteUrl}/rss`,
59 | site_url: `${siteConfig.siteUrl}/`,
60 | image_url: `${siteConfig.siteUrl}/${siteConfig.siteIcon}`,
61 | ttl: `60`,
62 | custom_namespaces: {
63 | content: `http://purl.org/rss/1.0/modules/content/`,
64 | media: `http://search.yahoo.com/mrss/`,
65 | },
66 | }
67 | return {
68 | ...feed,
69 | }
70 | },
71 | query: `
72 | {
73 | allGhostPost(sort: {order: DESC, fields: published_at}, filter: {primary_tag: {slug: {ne: "roundup"}}}) {
74 | edges {
75 | node {
76 | # Main fields
77 | id
78 | title
79 | slug
80 | featured
81 | feature_image
82 |
83 | # Dates unformatted
84 | created_at
85 | published_at
86 | updated_at
87 |
88 | # SEO
89 | excerpt
90 | meta_title
91 | meta_description
92 |
93 | # Authors
94 | authors {
95 | name
96 | }
97 | primary_author {
98 | name
99 | }
100 | tags {
101 | name
102 | visibility
103 | }
104 |
105 | # Content
106 | html
107 |
108 | # Additional fields
109 | url
110 | canonical_url
111 | }
112 | }
113 | }
114 | }
115 | `,
116 | output: `/rss`,
117 | title: siteConfig.siteTitleMeta,
118 | }
119 | }
120 |
121 | module.exports = generateRSSFeed
122 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/favicon.ico
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/favicon.png
--------------------------------------------------------------------------------
/static/fonts/AvenirNextLTPro-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/AvenirNextLTPro-Medium.woff
--------------------------------------------------------------------------------
/static/fonts/AvenirNextLTPro-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/AvenirNextLTPro-Medium.woff2
--------------------------------------------------------------------------------
/static/fonts/AvenirNextLTPro-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/AvenirNextLTPro-Regular.woff
--------------------------------------------------------------------------------
/static/fonts/AvenirNextLTPro-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/AvenirNextLTPro-Regular.woff2
--------------------------------------------------------------------------------
/static/fonts/FFMarkWebProBook.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/FFMarkWebProBook.woff
--------------------------------------------------------------------------------
/static/fonts/FFMarkWebProBook.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/FFMarkWebProBook.woff2
--------------------------------------------------------------------------------
/static/fonts/FFMarkWebProMedium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/FFMarkWebProMedium.woff
--------------------------------------------------------------------------------
/static/fonts/FFMarkWebProMedium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/fonts/FFMarkWebProMedium.woff2
--------------------------------------------------------------------------------
/static/images/counter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/images/cover.jpg
--------------------------------------------------------------------------------
/static/images/icons/avatar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/facebook.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/rss.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/images/logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/toddbirchard/gatsby-ghost-tokyo/dab02df4c79be76d9c61e31b6e03250080c55789/static/images/logo@2x.png
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 |
--------------------------------------------------------------------------------