Initial commit from gatsby: (https://github.com/TryGhost/gatsby-starter-ghost.git)
26
.editorconfig
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.hbs]
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
public/**
|
||||||
|
plugins/**/*.js
|
||||||
|
!plugins/*/src/*.js
|
59
.eslintrc.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
module.exports = {
|
||||||
|
'parser': 'babel-eslint',
|
||||||
|
'parserOptions': {
|
||||||
|
'ecmaVersion': 6,
|
||||||
|
'ecmaFeatures': {
|
||||||
|
'jsx': true,
|
||||||
|
'experimentalObjectRestSpread': true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: ['ghost', 'react'],
|
||||||
|
extends: [
|
||||||
|
'plugin:ghost/node',
|
||||||
|
'plugin:ghost/ember',
|
||||||
|
'plugin:react/recommended'
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"createClass": "createReactClass",
|
||||||
|
"pragma": "React",
|
||||||
|
"version": "16.0",
|
||||||
|
"flowVersion": "0.53"
|
||||||
|
},
|
||||||
|
"propWrapperFunctions": ["forbidExtraProps"]
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"ghost/sort-imports-es6-autofix/sort-imports-es6": "off",
|
||||||
|
"ghost/ember/use-ember-get-and-set": "off",
|
||||||
|
"no-console": "off",
|
||||||
|
"no-inner-declarations": "off",
|
||||||
|
"valid-jsdoc": "off",
|
||||||
|
"require-jsdoc": "off",
|
||||||
|
"quotes": ["error", "backtick"],
|
||||||
|
"consistent-return": ["error"],
|
||||||
|
"arrow-body-style": [
|
||||||
|
"error",
|
||||||
|
"as-needed",
|
||||||
|
{ "requireReturnForObjectLiteral": true }
|
||||||
|
],
|
||||||
|
"jsx-quotes": ["error", "prefer-double"],
|
||||||
|
"semi": ["error", "never"],
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"comma-dangle": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"arrays": "always-multiline",
|
||||||
|
"objects": "always-multiline",
|
||||||
|
"imports": "always-multiline",
|
||||||
|
"exports": "always-multiline",
|
||||||
|
"functions": "ignore"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"react/prop-types": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignore": ["children"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
10
.ghost.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"development": {
|
||||||
|
"apiUrl": "https://gatsby.ghost.io",
|
||||||
|
"contentApiKey": "9cc5c67c358edfdd81455149d0"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"apiUrl": "https://gatsby.ghost.io",
|
||||||
|
"contentApiKey": "9cc5c67c358edfdd81455149d0"
|
||||||
|
}
|
||||||
|
}
|
36
.github/ISSUE_TEMPLATE/---bug-report.md
vendored
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
name: "\U0001F41B Bug report"
|
||||||
|
about: Report reproducible software issues so we can improve
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to the Gatsby Starter Ghost GitHub repo! 👋🎉
|
||||||
|
|
||||||
|
We use GitHub only for bug reports 🐛
|
||||||
|
|
||||||
|
Anything else should be posted to https://forum.ghost.org 👫
|
||||||
|
|
||||||
|
For questions related to the usage of Gatsby or GraphQL, please check out their docs at https://www.gatsbyjs.org/ and https://graphql.org/
|
||||||
|
|
||||||
|
🚨For support, help & questions use https://forum.ghost.org/c/help
|
||||||
|
💡For feature requests & ideas you can post and vote on https://forum.ghost.org/c/Ideas
|
||||||
|
|
||||||
|
If your issue is with Gatsby.js itself, please report it at the Gatsby repo ➡️ https://github.com/gatsbyjs/gatsby/issues/new.
|
||||||
|
|
||||||
|
### Issue Summary
|
||||||
|
|
||||||
|
A summary of the issue and the browser/OS environment in which it occurs.
|
||||||
|
|
||||||
|
### To Reproduce
|
||||||
|
|
||||||
|
1. This is the first step
|
||||||
|
2. This is the second step, etc.
|
||||||
|
|
||||||
|
Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?
|
||||||
|
|
||||||
|
### Technical details:
|
||||||
|
|
||||||
|
* Ghost Version:
|
||||||
|
* Gatsby Version:
|
||||||
|
* Node Version:
|
||||||
|
* OS:
|
25
.github/ISSUE_TEMPLATE/--anything-else.md
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
name: "\U0001F4A1Anything else"
|
||||||
|
about: "For help, support, features & ideas - please use https://forum.ghost.org \U0001F46B "
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
--------------^ Click "Preview" for a nicer view!
|
||||||
|
|
||||||
|
We use GitHub only for bug reports 🐛
|
||||||
|
|
||||||
|
Anything else should be posted to https://forum.ghost.org 👫.
|
||||||
|
|
||||||
|
🚨For support, help & questions use https://forum.ghost.org/c/help
|
||||||
|
💡For feature requests & ideas you can post and vote on https://forum.ghost.org/c/Ideas
|
||||||
|
|
||||||
|
Alternatively, check out these resources below. Thanks! 😁.
|
||||||
|
|
||||||
|
- [Forum](https://forum.ghost.org/c/help)
|
||||||
|
- [Gatsby API reference](https://docs.ghost.org/api/gatsby/)
|
||||||
|
- [Content API Docs](https://docs.ghost.org/api/content/)
|
||||||
|
- [Gatsby.js](https://www.gatsbyjs.org)
|
||||||
|
- [GraphQL](https://graphql.org/)
|
||||||
|
- [Feature Requests / Ideas](https://forum.ghost.org/c/Ideas)
|
||||||
|
- [Contributing Guide](https://docs.ghost.org/docs/contributing)
|
||||||
|
- [Self-hoster Docs](https://docs.ghost.org/)
|
75
.gitignore
vendored
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# Node template
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Typescript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/*
|
||||||
|
*.iml
|
||||||
|
*.sublime-*
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Docs Custom
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
yarn-error.log
|
||||||
|
.netlify/
|
22
LICENSE
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright (c) 2013-2019 Ghost Foundation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person
|
||||||
|
obtaining a copy of this software and associated documentation
|
||||||
|
files (the "Software"), to deal in the Software without
|
||||||
|
restriction, including without limitation the rights to use,
|
||||||
|
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
85
README.md
Executable file
|
@ -0,0 +1,85 @@
|
||||||
|
# Gatsby Starter Ghost
|
||||||
|
|
||||||
|
A starter template to build lightning fast websites with [Ghost](https://ghost.org) & [Gatsby](https://gatsbyjs.org)
|
||||||
|
|
||||||
|
**Demo:** https://gatsby.ghost.org
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
![gatsby-starter-ghost](https://user-images.githubusercontent.com/120485/50913567-8ab8e380-142c-11e9-9e78-de02ded12fc6.jpg)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Installing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With Gatsby CLI
|
||||||
|
gatsby new gatsby-starter-ghost https://github.com/TryGhost/gatsby-starter-ghost.git
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From Source
|
||||||
|
git clone https://github.com/TryGhost/gatsby-starter-ghost.git
|
||||||
|
cd gatsby-starter-ghost
|
||||||
|
```
|
||||||
|
|
||||||
|
Then install dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Running
|
||||||
|
|
||||||
|
Start the development server. You now have a Gatsby site pulling content from headless Ghost.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gatsby develop
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the starter will populate content from a default Ghost install located at https://gatsby.ghost.io.
|
||||||
|
|
||||||
|
To use your own install, edit the `.ghost.json` config file with your credentials. You can find your `contentApiKey` in the "Integrations" screen in Ghost Admin. The minimum required version for Ghost is `2.10.0` in order to use this starter without issues.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Deploying with Netlify
|
||||||
|
|
||||||
|
The starter contains three config files specifically for deploying with Netlify. A `netlify.toml` file for build settings, a `/static/_headers` file with default security headers set for all routes, and `/static/_redirects` to set Netlify custom domain redirects.
|
||||||
|
|
||||||
|
To deploy to your Netlify account, hit the button below.
|
||||||
|
|
||||||
|
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/TryGhost/gatsby-starter-ghost)
|
||||||
|
|
||||||
|
Content API Keys are generally not considered to be sensitive information, they exist so that they can be changed in the event of abuse; so most people commit it directly to their `.ghost.json` config file. If you prefer to keep this information out of your repository you can remove this config and set [Netlify ENV variables](https://www.netlify.com/docs/continuous-deployment/#build-environment-variables) for production builds instead.
|
||||||
|
|
||||||
|
Once deployed, you can set up a [Ghost + Netlify Integration](https://docs.ghost.org/integrations/netlify/) to use deploy hooks from Ghost to trigger Netlify rebuilds. That way, any time data changes in Ghost, your site will rebuild on Netlify.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Optimising
|
||||||
|
|
||||||
|
You can disable the default Ghost Handlebars Theme front-end by enabling the `Make this site private` flag within your Ghost settings. This enables password protection in front of the Ghost install and sets `<meta name="robots" content="noindex" />` so your Gatsby front-end becomes the source of truth for SEO.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Extra options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run a production build, locally
|
||||||
|
gatsby build
|
||||||
|
|
||||||
|
# Serve a production build, locally
|
||||||
|
gatsby serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Gatsby `develop` uses the `development` config in `.ghost.json` - while Gatsby `build` uses the `production` config.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Copyright & License
|
||||||
|
|
||||||
|
Copyright (c) 2013-2019 Ghost Foundation - Released under the [MIT license](LICENSE).
|
32
gatsby-browser.js
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* Trust All Scripts
|
||||||
|
*
|
||||||
|
* This is a dirty little script for iterating over script tags
|
||||||
|
* of your Ghost posts and adding them to the document head.
|
||||||
|
*
|
||||||
|
* This works for any script that then injects content into the page
|
||||||
|
* via ids/classnames etc.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
var trustAllScripts = function () {
|
||||||
|
var scriptNodes = document.querySelectorAll('.load-external-scripts script');
|
||||||
|
|
||||||
|
for (var i = 0; i < scriptNodes.length; i += 1) {
|
||||||
|
var node = scriptNodes[i];
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.type = node.type || 'text/javascript';
|
||||||
|
|
||||||
|
if (node.attributes.src) {
|
||||||
|
s.src = node.attributes.src.value;
|
||||||
|
} else {
|
||||||
|
s.innerHTML = node.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementsByTagName('head')[0].appendChild(s);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.onRouteUpdate = function () {
|
||||||
|
trustAllScripts();
|
||||||
|
};
|
187
gatsby-config.js
Executable file
|
@ -0,0 +1,187 @@
|
||||||
|
const path = require(`path`)
|
||||||
|
|
||||||
|
const config = require(`./src/utils/siteConfig`)
|
||||||
|
const generateRSSFeed = require(`./src/utils/rss/generate-feed`)
|
||||||
|
|
||||||
|
let ghostConfig
|
||||||
|
|
||||||
|
try {
|
||||||
|
ghostConfig = require(`./.ghost`)
|
||||||
|
} catch (e) {
|
||||||
|
ghostConfig = {
|
||||||
|
production: {
|
||||||
|
apiUrl: process.env.GHOST_API_URL,
|
||||||
|
contentApiKey: process.env.GHOST_CONTENT_API_KEY,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const { apiUrl, contentApiKey } = process.env.NODE_ENV === `development` ? ghostConfig.development : ghostConfig.production
|
||||||
|
|
||||||
|
if (!apiUrl || !contentApiKey || contentApiKey.match(/<key>/)) {
|
||||||
|
throw new Error(`GHOST_API_URL and GHOST_CONTENT_API_KEY are required to build. Check the README.`) // eslint-disable-line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the place where you can tell Gatsby which plugins to use
|
||||||
|
* and set them up the way you want.
|
||||||
|
*
|
||||||
|
* Further info 👉🏼 https://www.gatsbyjs.org/docs/gatsby-config/
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
siteMetadata: {
|
||||||
|
siteUrl: config.siteUrl,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
/**
|
||||||
|
* Content Plugins
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
resolve: `gatsby-source-filesystem`,
|
||||||
|
options: {
|
||||||
|
path: path.join(__dirname, `src`, `pages`),
|
||||||
|
name: `pages`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Setup for optimised images.
|
||||||
|
// See https://www.gatsbyjs.org/packages/gatsby-image/
|
||||||
|
{
|
||||||
|
resolve: `gatsby-source-filesystem`,
|
||||||
|
options: {
|
||||||
|
path: path.join(__dirname, `src`, `images`),
|
||||||
|
name: `images`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`gatsby-plugin-sharp`,
|
||||||
|
`gatsby-transformer-sharp`,
|
||||||
|
{
|
||||||
|
resolve: `gatsby-source-ghost`,
|
||||||
|
options:
|
||||||
|
process.env.NODE_ENV === `development`
|
||||||
|
? ghostConfig.development
|
||||||
|
: ghostConfig.production,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Utility Plugins
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
resolve: `gatsby-plugin-ghost-manifest`,
|
||||||
|
options: {
|
||||||
|
short_name: config.shortTitle,
|
||||||
|
start_url: `/`,
|
||||||
|
background_color: config.backgroundColor,
|
||||||
|
theme_color: config.themeColor,
|
||||||
|
display: `minimal-ui`,
|
||||||
|
icon: `static/${config.siteIcon}`,
|
||||||
|
legacy: true,
|
||||||
|
query: `
|
||||||
|
{
|
||||||
|
allGhostSettings {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolve: `gatsby-plugin-feed`,
|
||||||
|
options: {
|
||||||
|
query: `
|
||||||
|
{
|
||||||
|
allGhostSettings {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
feeds: [
|
||||||
|
generateRSSFeed(config),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolve: `gatsby-plugin-advanced-sitemap`,
|
||||||
|
options: {
|
||||||
|
query: `
|
||||||
|
{
|
||||||
|
allGhostPost {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
updated_at
|
||||||
|
created_at
|
||||||
|
feature_image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGhostPage {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
updated_at
|
||||||
|
created_at
|
||||||
|
feature_image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGhostTag {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
feature_image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGhostAuthor {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
profile_image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
mapping: {
|
||||||
|
allGhostPost: {
|
||||||
|
sitemap: `posts`,
|
||||||
|
},
|
||||||
|
allGhostTag: {
|
||||||
|
sitemap: `tags`,
|
||||||
|
},
|
||||||
|
allGhostAuthor: {
|
||||||
|
sitemap: `authors`,
|
||||||
|
},
|
||||||
|
allGhostPage: {
|
||||||
|
sitemap: `pages`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
exclude: [
|
||||||
|
`/dev-404-page`,
|
||||||
|
`/404`,
|
||||||
|
`/404.html`,
|
||||||
|
`/offline-plugin-app-shell-fallback`,
|
||||||
|
],
|
||||||
|
createLinkInHead: true,
|
||||||
|
addUncaughtPages: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
`gatsby-plugin-catch-links`,
|
||||||
|
`gatsby-plugin-react-helmet`,
|
||||||
|
`gatsby-plugin-force-trailing-slashes`,
|
||||||
|
`gatsby-plugin-offline`,
|
||||||
|
],
|
||||||
|
}
|
210
gatsby-node.js
Executable file
|
@ -0,0 +1,210 @@
|
||||||
|
const path = require(`path`)
|
||||||
|
const { postsPerPage } = require(`./src/utils/siteConfig`)
|
||||||
|
const { paginate } = require(`gatsby-awesome-pagination`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Here is the place where Gatsby creates the URLs for all the
|
||||||
|
* posts, tags, pages and authors that we fetched from the Ghost site.
|
||||||
|
*/
|
||||||
|
exports.createPages = async ({ graphql, actions }) => {
|
||||||
|
const { createPage } = actions
|
||||||
|
|
||||||
|
const result = await graphql(`
|
||||||
|
{
|
||||||
|
allGhostPost(sort: { order: ASC, fields: published_at }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
slug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGhostTag(sort: { order: ASC, fields: name }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
slug
|
||||||
|
url
|
||||||
|
postCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGhostAuthor(sort: { order: ASC, fields: name }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
slug
|
||||||
|
url
|
||||||
|
postCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGhostPage(sort: { order: ASC, fields: published_at }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
slug
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allGhostPage(sort: { order: ASC, fields: published_at }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
slug
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Check for any errors
|
||||||
|
if (result.errors) {
|
||||||
|
throw new Error(result.errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract query results
|
||||||
|
const tags = result.data.allGhostTag.edges
|
||||||
|
const authors = result.data.allGhostAuthor.edges
|
||||||
|
const pages = result.data.allGhostPage.edges
|
||||||
|
const posts = result.data.allGhostPost.edges
|
||||||
|
|
||||||
|
// Load templates
|
||||||
|
const indexTemplate = path.resolve(`./src/templates/index.js`)
|
||||||
|
const tagsTemplate = path.resolve(`./src/templates/tag.js`)
|
||||||
|
const authorTemplate = path.resolve(`./src/templates/author.js`)
|
||||||
|
const pageTemplate = path.resolve(`./src/templates/page.js`)
|
||||||
|
const postTemplate = path.resolve(`./src/templates/post.js`)
|
||||||
|
|
||||||
|
// Create tag pages
|
||||||
|
tags.forEach(({ node }) => {
|
||||||
|
const totalPosts = node.postCount !== null ? node.postCount : 0
|
||||||
|
const numberOfPages = Math.ceil(totalPosts / postsPerPage)
|
||||||
|
|
||||||
|
// This part here defines, that our tag pages will use
|
||||||
|
// a `/tag/:slug/` permalink.
|
||||||
|
node.url = `/tag/${node.slug}/`
|
||||||
|
|
||||||
|
Array.from({ length: numberOfPages }).forEach((_, i) => {
|
||||||
|
const currentPage = i + 1
|
||||||
|
const prevPageNumber = currentPage <= 1 ? null : currentPage - 1
|
||||||
|
const nextPageNumber =
|
||||||
|
currentPage + 1 > numberOfPages ? null : currentPage + 1
|
||||||
|
const previousPagePath = prevPageNumber
|
||||||
|
? prevPageNumber === 1
|
||||||
|
? node.url
|
||||||
|
: `${node.url}page/${prevPageNumber}/`
|
||||||
|
: null
|
||||||
|
const nextPagePath = nextPageNumber
|
||||||
|
? `${node.url}page/${nextPageNumber}/`
|
||||||
|
: null
|
||||||
|
|
||||||
|
createPage({
|
||||||
|
path: i === 0 ? node.url : `${node.url}page/${i + 1}/`,
|
||||||
|
component: tagsTemplate,
|
||||||
|
context: {
|
||||||
|
// Data passed to context is available
|
||||||
|
// in page queries as GraphQL variables.
|
||||||
|
slug: node.slug,
|
||||||
|
limit: postsPerPage,
|
||||||
|
skip: i * postsPerPage,
|
||||||
|
numberOfPages: numberOfPages,
|
||||||
|
humanPageNumber: currentPage,
|
||||||
|
prevPageNumber: prevPageNumber,
|
||||||
|
nextPageNumber: nextPageNumber,
|
||||||
|
previousPagePath: previousPagePath,
|
||||||
|
nextPagePath: nextPagePath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create author pages
|
||||||
|
authors.forEach(({ node }) => {
|
||||||
|
const totalPosts = node.postCount !== null ? node.postCount : 0
|
||||||
|
const numberOfPages = Math.ceil(totalPosts / postsPerPage)
|
||||||
|
|
||||||
|
// This part here defines, that our author pages will use
|
||||||
|
// a `/author/:slug/` permalink.
|
||||||
|
node.url = `/author/${node.slug}/`
|
||||||
|
|
||||||
|
Array.from({ length: numberOfPages }).forEach((_, i) => {
|
||||||
|
const currentPage = i + 1
|
||||||
|
const prevPageNumber = currentPage <= 1 ? null : currentPage - 1
|
||||||
|
const nextPageNumber =
|
||||||
|
currentPage + 1 > numberOfPages ? null : currentPage + 1
|
||||||
|
const previousPagePath = prevPageNumber
|
||||||
|
? prevPageNumber === 1
|
||||||
|
? node.url
|
||||||
|
: `${node.url}page/${prevPageNumber}/`
|
||||||
|
: null
|
||||||
|
const nextPagePath = nextPageNumber
|
||||||
|
? `${node.url}page/${nextPageNumber}/`
|
||||||
|
: null
|
||||||
|
|
||||||
|
createPage({
|
||||||
|
path: i === 0 ? node.url : `${node.url}page/${i + 1}/`,
|
||||||
|
component: authorTemplate,
|
||||||
|
context: {
|
||||||
|
// Data passed to context is available
|
||||||
|
// in page queries as GraphQL variables.
|
||||||
|
slug: node.slug,
|
||||||
|
limit: postsPerPage,
|
||||||
|
skip: i * postsPerPage,
|
||||||
|
numberOfPages: numberOfPages,
|
||||||
|
humanPageNumber: currentPage,
|
||||||
|
prevPageNumber: prevPageNumber,
|
||||||
|
nextPageNumber: nextPageNumber,
|
||||||
|
previousPagePath: previousPagePath,
|
||||||
|
nextPagePath: nextPagePath,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create pages
|
||||||
|
pages.forEach(({ node }) => {
|
||||||
|
// This part here defines, that our pages will use
|
||||||
|
// a `/:slug/` permalink.
|
||||||
|
node.url = `/${node.slug}/`
|
||||||
|
|
||||||
|
createPage({
|
||||||
|
path: node.url,
|
||||||
|
component: pageTemplate,
|
||||||
|
context: {
|
||||||
|
// Data passed to context is available
|
||||||
|
// in page queries as GraphQL variables.
|
||||||
|
slug: node.slug,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create post pages
|
||||||
|
posts.forEach(({ node }) => {
|
||||||
|
// This part here defines, that our posts will use
|
||||||
|
// a `/:slug/` permalink.
|
||||||
|
node.url = `/${node.slug}/`
|
||||||
|
|
||||||
|
createPage({
|
||||||
|
path: node.url,
|
||||||
|
component: postTemplate,
|
||||||
|
context: {
|
||||||
|
// Data passed to context is available
|
||||||
|
// in page queries as GraphQL variables.
|
||||||
|
slug: node.slug,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create pagination
|
||||||
|
paginate({
|
||||||
|
createPage,
|
||||||
|
items: posts,
|
||||||
|
itemsPerPage: postsPerPage,
|
||||||
|
component: indexTemplate,
|
||||||
|
pathPrefix: ({ pageNumber }) => {
|
||||||
|
if (pageNumber === 0) {
|
||||||
|
return `/`
|
||||||
|
} else {
|
||||||
|
return `/page`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
6
netlify.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[build]
|
||||||
|
command = "gatsby build"
|
||||||
|
publish = "public/"
|
||||||
|
|
||||||
|
[template]
|
||||||
|
incoming-hooks = ["Ghost"]
|
61
package.json
Executable file
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"name": "gatsby-starter-ghost",
|
||||||
|
"description": "A starter template to build lightning fast websites with Ghost and Gatsby",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"author": "Ghost Foundation",
|
||||||
|
"homepage": "https://docs.ghost.org/api/gatsby/",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/tryghost/gatsby-starter-ghost.git"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"sharp": "0.22.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.9.0"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/tryghost/gatsby-starter-ghost/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"gatsby",
|
||||||
|
"ghost"
|
||||||
|
],
|
||||||
|
"main": "n/a",
|
||||||
|
"scripts": {
|
||||||
|
"serve": "gatsby build && NODE_ENV=production gatsby serve",
|
||||||
|
"build": "gatsby build",
|
||||||
|
"dev": "gatsby develop",
|
||||||
|
"lint": "eslint . --ext .js --cache",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "6.4.0",
|
||||||
|
"eslint-plugin-ghost": "0.5.0",
|
||||||
|
"eslint-plugin-react": "7.14.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tryghost/helpers": "1.1.10",
|
||||||
|
"@tryghost/helpers-gatsby": "1.0.13",
|
||||||
|
"cheerio": "1.0.0-rc.3",
|
||||||
|
"gatsby": "2.15.18",
|
||||||
|
"gatsby-awesome-pagination": "0.3.4",
|
||||||
|
"gatsby-image": "2.2.19",
|
||||||
|
"gatsby-plugin-advanced-sitemap": "1.4.4",
|
||||||
|
"gatsby-plugin-catch-links": "2.1.9",
|
||||||
|
"gatsby-plugin-feed": "2.3.12",
|
||||||
|
"gatsby-plugin-force-trailing-slashes": "1.0.4",
|
||||||
|
"gatsby-plugin-manifest": "2.2.17",
|
||||||
|
"gatsby-plugin-offline": "3.0.7",
|
||||||
|
"gatsby-plugin-react-helmet": "3.1.7",
|
||||||
|
"gatsby-plugin-sharp": "2.2.24",
|
||||||
|
"gatsby-source-filesystem": "2.1.24",
|
||||||
|
"gatsby-source-ghost": "3.4.6",
|
||||||
|
"gatsby-transformer-sharp": "2.2.15",
|
||||||
|
"lodash": "4.17.15",
|
||||||
|
"react": "16.9.0",
|
||||||
|
"react-dom": "16.9.0",
|
||||||
|
"react-helmet": "5.2.1"
|
||||||
|
}
|
||||||
|
}
|
10
plugins/gatsby-plugin-ghost-manifest/.babelrc
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"babel-preset-gatsby-package",
|
||||||
|
{
|
||||||
|
"browser": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
55
plugins/gatsby-plugin-ghost-manifest/common.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var fs = require("fs"); // default icons for generating icons
|
||||||
|
|
||||||
|
|
||||||
|
exports.defaultIcons = [{
|
||||||
|
src: "icons/icon-48x48.png",
|
||||||
|
sizes: "48x48",
|
||||||
|
type: "image/png"
|
||||||
|
}, {
|
||||||
|
src: "icons/icon-72x72.png",
|
||||||
|
sizes: "72x72",
|
||||||
|
type: "image/png"
|
||||||
|
}, {
|
||||||
|
src: "icons/icon-96x96.png",
|
||||||
|
sizes: "96x96",
|
||||||
|
type: "image/png"
|
||||||
|
}, {
|
||||||
|
src: "icons/icon-144x144.png",
|
||||||
|
sizes: "144x144",
|
||||||
|
type: "image/png"
|
||||||
|
}, {
|
||||||
|
src: "icons/icon-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png"
|
||||||
|
}, {
|
||||||
|
src: "icons/icon-256x256.png",
|
||||||
|
sizes: "256x256",
|
||||||
|
type: "image/png"
|
||||||
|
}, {
|
||||||
|
src: "icons/icon-384x384.png",
|
||||||
|
sizes: "384x384",
|
||||||
|
type: "image/png"
|
||||||
|
}, {
|
||||||
|
src: "icons/icon-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png"
|
||||||
|
}];
|
||||||
|
/**
|
||||||
|
* Check if the icon exists on the filesystem
|
||||||
|
*
|
||||||
|
* @param {String} srcIcon Path of the icon
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.doesIconExist = function doesIconExist(srcIcon) {
|
||||||
|
try {
|
||||||
|
return fs.statSync(srcIcon).isFile();
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === "ENOENT") {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
104
plugins/gatsby-plugin-ghost-manifest/gatsby-node.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
||||||
|
|
||||||
|
var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
|
||||||
|
|
||||||
|
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
|
||||||
|
|
||||||
|
var _objectWithoutPropertiesLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutPropertiesLoose"));
|
||||||
|
|
||||||
|
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
|
||||||
|
|
||||||
|
var fs = require("fs");
|
||||||
|
|
||||||
|
var path = require("path");
|
||||||
|
|
||||||
|
var Promise = require("bluebird");
|
||||||
|
|
||||||
|
var sharp = require("sharp");
|
||||||
|
|
||||||
|
var _require = require("./common.js"),
|
||||||
|
defaultIcons = _require.defaultIcons,
|
||||||
|
doesIconExist = _require.doesIconExist;
|
||||||
|
|
||||||
|
sharp.simd(true);
|
||||||
|
|
||||||
|
function generateIcons(icons, srcIcon) {
|
||||||
|
return Promise.map(icons, function (icon) {
|
||||||
|
var size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf("x")));
|
||||||
|
var imgPath = path.join("public", icon.src);
|
||||||
|
return sharp(srcIcon).resize(size).toFile(imgPath).then(function () {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.onPostBuild =
|
||||||
|
/*#__PURE__*/
|
||||||
|
function () {
|
||||||
|
var _ref2 = (0, _asyncToGenerator2.default)(
|
||||||
|
/*#__PURE__*/
|
||||||
|
_regenerator.default.mark(function _callee(_ref, pluginOptions) {
|
||||||
|
var graphql, icon, manifest, _ref3, data, siteTitle, iconPath;
|
||||||
|
|
||||||
|
return _regenerator.default.wrap(function _callee$(_context) {
|
||||||
|
while (1) {
|
||||||
|
switch (_context.prev = _context.next) {
|
||||||
|
case 0:
|
||||||
|
graphql = _ref.graphql;
|
||||||
|
icon = pluginOptions.icon, manifest = (0, _objectWithoutPropertiesLoose2.default)(pluginOptions, ["icon"]);
|
||||||
|
_context.next = 4;
|
||||||
|
return graphql(pluginOptions.query);
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
_ref3 = _context.sent;
|
||||||
|
data = _ref3.data;
|
||||||
|
siteTitle = data.allGhostSettings.edges[0].node.title || "No Title";
|
||||||
|
manifest = (0, _extends2.default)({}, manifest, {
|
||||||
|
name: siteTitle
|
||||||
|
}); // Delete options we won't pass to the manifest.webmanifest.
|
||||||
|
|
||||||
|
delete manifest.plugins;
|
||||||
|
delete manifest.legacy;
|
||||||
|
delete manifest.theme_color_in_head;
|
||||||
|
delete manifest.query; // If icons are not manually defined, use the default icon set.
|
||||||
|
|
||||||
|
if (!manifest.icons) {
|
||||||
|
manifest.icons = defaultIcons;
|
||||||
|
} // Determine destination path for icons.
|
||||||
|
|
||||||
|
|
||||||
|
iconPath = path.join("public", path.dirname(manifest.icons[0].src)); //create destination directory if it doesn't exist
|
||||||
|
|
||||||
|
if (!fs.existsSync(iconPath)) {
|
||||||
|
fs.mkdirSync(iconPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join("public", "manifest.webmanifest"), JSON.stringify(manifest)); // Only auto-generate icons if a src icon is defined.
|
||||||
|
|
||||||
|
if (icon !== undefined) {
|
||||||
|
// Check if the icon exists
|
||||||
|
if (!doesIconExist(icon)) {
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
generateIcons(manifest.icons, icon).then(function () {
|
||||||
|
//images have been generated
|
||||||
|
console.log("done generating icons for manifest");
|
||||||
|
Promise.resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
case 17:
|
||||||
|
case "end":
|
||||||
|
return _context.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, _callee);
|
||||||
|
}));
|
||||||
|
|
||||||
|
return function (_x, _x2) {
|
||||||
|
return _ref2.apply(this, arguments);
|
||||||
|
};
|
||||||
|
}();
|
83
plugins/gatsby-plugin-ghost-manifest/gatsby-ssr.js
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
||||||
|
|
||||||
|
var _react = _interopRequireDefault(require("react"));
|
||||||
|
|
||||||
|
var _gatsby = require("gatsby");
|
||||||
|
|
||||||
|
var _common = require("./common.js");
|
||||||
|
|
||||||
|
var _jsxFileName = "/Users/aileen/code/gatsby-starter-ghost/plugins/gatsby-plugin-ghost-manifest/src/gatsby-ssr.js";
|
||||||
|
|
||||||
|
exports.onRenderBody = function (_ref, pluginOptions) {
|
||||||
|
var setHeadComponents = _ref.setHeadComponents;
|
||||||
|
// We use this to build a final array to pass as the argument to setHeadComponents at the end of onRenderBody.
|
||||||
|
var headComponents = [];
|
||||||
|
var icons = pluginOptions.icons || _common.defaultIcons; // If icons were generated, also add a favicon link.
|
||||||
|
|
||||||
|
if (pluginOptions.icon) {
|
||||||
|
var favicon = icons && icons.length ? icons[0].src : null;
|
||||||
|
|
||||||
|
if (favicon) {
|
||||||
|
headComponents.push(_react.default.createElement("link", {
|
||||||
|
key: "gatsby-plugin-manifest-icon-link",
|
||||||
|
rel: "shortcut icon",
|
||||||
|
href: (0, _gatsby.withPrefix)(favicon),
|
||||||
|
__source: {
|
||||||
|
fileName: _jsxFileName,
|
||||||
|
lineNumber: 17
|
||||||
|
},
|
||||||
|
__self: this
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} // Add manifest link tag.
|
||||||
|
|
||||||
|
|
||||||
|
headComponents.push(_react.default.createElement("link", {
|
||||||
|
key: "gatsby-plugin-manifest-link",
|
||||||
|
rel: "manifest",
|
||||||
|
href: (0, _gatsby.withPrefix)("/manifest.webmanifest"),
|
||||||
|
__source: {
|
||||||
|
fileName: _jsxFileName,
|
||||||
|
lineNumber: 28
|
||||||
|
},
|
||||||
|
__self: this
|
||||||
|
})); // The user has an option to opt out of the theme_color meta tag being inserted into the head.
|
||||||
|
|
||||||
|
if (pluginOptions.theme_color) {
|
||||||
|
var insertMetaTag = Object.keys(pluginOptions).includes("theme_color_in_head") ? pluginOptions.theme_color_in_head : true;
|
||||||
|
|
||||||
|
if (insertMetaTag) {
|
||||||
|
headComponents.push(_react.default.createElement("meta", {
|
||||||
|
key: "gatsby-plugin-manifest-meta",
|
||||||
|
name: "theme-color",
|
||||||
|
content: pluginOptions.theme_color,
|
||||||
|
__source: {
|
||||||
|
fileName: _jsxFileName,
|
||||||
|
lineNumber: 44
|
||||||
|
},
|
||||||
|
__self: this
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginOptions.legacy) {
|
||||||
|
var iconLinkTags = icons.map(function (icon) {
|
||||||
|
return _react.default.createElement("link", {
|
||||||
|
key: "gatsby-plugin-manifest-apple-touch-icon-" + icon.sizes,
|
||||||
|
rel: "apple-touch-icon",
|
||||||
|
sizes: icon.sizes,
|
||||||
|
href: (0, _gatsby.withPrefix)("" + icon.src),
|
||||||
|
__source: {
|
||||||
|
fileName: _jsxFileName,
|
||||||
|
lineNumber: 55
|
||||||
|
},
|
||||||
|
__self: this
|
||||||
|
});
|
||||||
|
});
|
||||||
|
headComponents = [].concat(headComponents, iconLinkTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeadComponents(headComponents);
|
||||||
|
};
|
1
plugins/gatsby-plugin-ghost-manifest/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
// noop
|
39
plugins/gatsby-plugin-ghost-manifest/package.json
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "gatsby-plugin-ghost-manifest",
|
||||||
|
"description": "Gatsby plugin which adds a manifest.webmanifest to make sites progressive web apps",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"author": "Ghost Foundation",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "7.6.0",
|
||||||
|
"bluebird": "3.5.5",
|
||||||
|
"sharp": "0.22.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "7.6.0",
|
||||||
|
"@babel/core": "7.6.0",
|
||||||
|
"babel-preset-gatsby-package": "0.2.4",
|
||||||
|
"cross-env": "6.0.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"gatsby",
|
||||||
|
"gatsby-plugin",
|
||||||
|
"favicon",
|
||||||
|
"icons",
|
||||||
|
"manifest.webmanifest",
|
||||||
|
"progressive-web-app",
|
||||||
|
"pwa"
|
||||||
|
],
|
||||||
|
"resolutions": {
|
||||||
|
"sharp": "0.22.1"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "index.js",
|
||||||
|
"peerDependencies": {
|
||||||
|
"gatsby": ">2.0.0-alpha"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "babel src --out-dir . --ignore **/__tests__",
|
||||||
|
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||||
|
"watch": "babel -w src --out-dir . --ignore **/__tests__"
|
||||||
|
}
|
||||||
|
}
|
62
plugins/gatsby-plugin-ghost-manifest/src/common.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
const fs = require(`fs`)
|
||||||
|
|
||||||
|
// default icons for generating icons
|
||||||
|
exports.defaultIcons = [
|
||||||
|
{
|
||||||
|
src: `icons/icon-48x48.png`,
|
||||||
|
sizes: `48x48`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `icons/icon-72x72.png`,
|
||||||
|
sizes: `72x72`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `icons/icon-96x96.png`,
|
||||||
|
sizes: `96x96`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `icons/icon-144x144.png`,
|
||||||
|
sizes: `144x144`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `icons/icon-192x192.png`,
|
||||||
|
sizes: `192x192`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `icons/icon-256x256.png`,
|
||||||
|
sizes: `256x256`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `icons/icon-384x384.png`,
|
||||||
|
sizes: `384x384`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: `icons/icon-512x512.png`,
|
||||||
|
sizes: `512x512`,
|
||||||
|
type: `image/png`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the icon exists on the filesystem
|
||||||
|
*
|
||||||
|
* @param {String} srcIcon Path of the icon
|
||||||
|
*/
|
||||||
|
exports.doesIconExist = function doesIconExist(srcIcon) {
|
||||||
|
try {
|
||||||
|
return fs.statSync(srcIcon).isFile()
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === `ENOENT`) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
plugins/gatsby-plugin-ghost-manifest/src/gatsby-node.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
const fs = require(`fs`)
|
||||||
|
const path = require(`path`)
|
||||||
|
const Promise = require(`bluebird`)
|
||||||
|
const sharp = require(`sharp`)
|
||||||
|
const { defaultIcons, doesIconExist } = require(`./common.js`)
|
||||||
|
|
||||||
|
sharp.simd(true)
|
||||||
|
|
||||||
|
function generateIcons(icons, srcIcon) {
|
||||||
|
return Promise.map(icons, (icon) => {
|
||||||
|
const size = parseInt(icon.sizes.substring(0, icon.sizes.lastIndexOf(`x`)))
|
||||||
|
const imgPath = path.join(`public`, icon.src)
|
||||||
|
|
||||||
|
return sharp(srcIcon)
|
||||||
|
.resize(size)
|
||||||
|
.toFile(imgPath)
|
||||||
|
.then(() => { })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.onPostBuild = async ({ graphql }, pluginOptions) => {
|
||||||
|
let { icon, ...manifest } = pluginOptions
|
||||||
|
|
||||||
|
const { data } = await graphql(pluginOptions.query)
|
||||||
|
const siteTitle = data.allGhostSettings.edges[0].node.title || `No Title`
|
||||||
|
manifest = {
|
||||||
|
...manifest,
|
||||||
|
name: siteTitle,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete options we won't pass to the manifest.webmanifest.
|
||||||
|
delete manifest.plugins
|
||||||
|
delete manifest.legacy
|
||||||
|
delete manifest.theme_color_in_head
|
||||||
|
delete manifest.query
|
||||||
|
|
||||||
|
// If icons are not manually defined, use the default icon set.
|
||||||
|
if (!manifest.icons) {
|
||||||
|
manifest.icons = defaultIcons
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine destination path for icons.
|
||||||
|
const iconPath = path.join(`public`, path.dirname(manifest.icons[0].src))
|
||||||
|
|
||||||
|
//create destination directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(iconPath)) {
|
||||||
|
fs.mkdirSync(iconPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(`public`, `manifest.webmanifest`),
|
||||||
|
JSON.stringify(manifest)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Only auto-generate icons if a src icon is defined.
|
||||||
|
if (icon !== undefined) {
|
||||||
|
// Check if the icon exists
|
||||||
|
if (!doesIconExist(icon)) {
|
||||||
|
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.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
generateIcons(manifest.icons, icon).then(() => {
|
||||||
|
//images have been generated
|
||||||
|
console.log(`done generating icons for manifest`)
|
||||||
|
Promise.resolve()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
67
plugins/gatsby-plugin-ghost-manifest/src/gatsby-ssr.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import React from "react"
|
||||||
|
import { withPrefix } from "gatsby"
|
||||||
|
import { defaultIcons } from "./common.js"
|
||||||
|
|
||||||
|
exports.onRenderBody = ({ setHeadComponents }, pluginOptions) => {
|
||||||
|
// We use this to build a final array to pass as the argument to setHeadComponents at the end of onRenderBody.
|
||||||
|
let headComponents = []
|
||||||
|
|
||||||
|
const icons = pluginOptions.icons || defaultIcons
|
||||||
|
|
||||||
|
// If icons were generated, also add a favicon link.
|
||||||
|
if (pluginOptions.icon) {
|
||||||
|
let favicon = icons && icons.length ? icons[0].src : null
|
||||||
|
|
||||||
|
if (favicon) {
|
||||||
|
headComponents.push(
|
||||||
|
<link
|
||||||
|
key={`gatsby-plugin-manifest-icon-link`}
|
||||||
|
rel="shortcut icon"
|
||||||
|
href={withPrefix(favicon)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add manifest link tag.
|
||||||
|
headComponents.push(
|
||||||
|
<link
|
||||||
|
key={`gatsby-plugin-manifest-link`}
|
||||||
|
rel="manifest"
|
||||||
|
href={withPrefix(`/manifest.webmanifest`)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
// The user has an option to opt out of the theme_color meta tag being inserted into the head.
|
||||||
|
if (pluginOptions.theme_color) {
|
||||||
|
let insertMetaTag = Object.keys(pluginOptions).includes(
|
||||||
|
`theme_color_in_head`
|
||||||
|
)
|
||||||
|
? pluginOptions.theme_color_in_head
|
||||||
|
: true
|
||||||
|
|
||||||
|
if (insertMetaTag) {
|
||||||
|
headComponents.push(
|
||||||
|
<meta
|
||||||
|
key={`gatsby-plugin-manifest-meta`}
|
||||||
|
name="theme-color"
|
||||||
|
content={pluginOptions.theme_color}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginOptions.legacy) {
|
||||||
|
const iconLinkTags = icons.map(icon => (
|
||||||
|
<link
|
||||||
|
key={`gatsby-plugin-manifest-apple-touch-icon-${icon.sizes}`}
|
||||||
|
rel="apple-touch-icon"
|
||||||
|
sizes={icon.sizes}
|
||||||
|
href={withPrefix(`${icon.src}`)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
headComponents = [...headComponents, ...iconLinkTags]
|
||||||
|
}
|
||||||
|
|
||||||
|
setHeadComponents(headComponents)
|
||||||
|
}
|
2599
plugins/gatsby-plugin-ghost-manifest/yarn.lock
Normal file
134
src/components/common/Layout.js
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import Helmet from 'react-helmet'
|
||||||
|
import { Link, StaticQuery, graphql } from 'gatsby'
|
||||||
|
import Img from 'gatsby-image'
|
||||||
|
|
||||||
|
import { Navigation } from '.'
|
||||||
|
import config from '../../utils/siteConfig'
|
||||||
|
|
||||||
|
// Styles
|
||||||
|
import '../../styles/app.css'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main layout component
|
||||||
|
*
|
||||||
|
* The Layout component wraps around each page and template.
|
||||||
|
* It also provides the header, footer as well as the main
|
||||||
|
* styles, and meta data for each page.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const DefaultLayout = ({ data, children, bodyClass, isHome }) => {
|
||||||
|
const site = data.allGhostSettings.edges[0].node
|
||||||
|
const twitterUrl = site.twitter ? `https://twitter.com/${site.twitter.replace(/^@/, ``)}` : null
|
||||||
|
const facebookUrl = site.facebook ? `https://www.facebook.com/${site.facebook.replace(/^\//, ``)}` : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<html lang={site.lang} />
|
||||||
|
<style type="text/css">{`${site.codeinjection_styles}`}</style>
|
||||||
|
<body className={bodyClass} />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="viewport">
|
||||||
|
|
||||||
|
<div className="viewport-top">
|
||||||
|
{/* The main header section on top of the screen */}
|
||||||
|
<header className="site-head" style={{ ...site.cover_image && { backgroundImage: `url(${site.cover_image})` } }}>
|
||||||
|
<div className="container">
|
||||||
|
<div className="site-mast">
|
||||||
|
<div className="site-mast-left">
|
||||||
|
<Link to="/">
|
||||||
|
{site.logo ?
|
||||||
|
<img className="site-logo" src={site.logo} alt={site.title} />
|
||||||
|
: <Img fixed={data.file.childImageSharp.fixed} alt={site.title} />
|
||||||
|
}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="site-mast-right">
|
||||||
|
{ site.twitter && <a href={ twitterUrl } className="site-nav-item" target="_blank" rel="noopener noreferrer"><img className="site-nav-icon" src="/images/icons/twitter.svg" alt="Twitter" /></a>}
|
||||||
|
{ site.facebook && <a href={ facebookUrl } className="site-nav-item" target="_blank" rel="noopener noreferrer"><img className="site-nav-icon" src="/images/icons/facebook.svg" alt="Facebook" /></a>}
|
||||||
|
<a className="site-nav-item" href={ `https://feedly.com/i/subscription/feed/${config.siteUrl}/rss/` } target="_blank" rel="noopener noreferrer"><img className="site-nav-icon" src="/images/icons/rss.svg" alt="RSS Feed" /></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ isHome ?
|
||||||
|
<div className="site-banner">
|
||||||
|
<h1 className="site-banner-title">{site.title}</h1>
|
||||||
|
<p className="site-banner-desc">{site.description}</p>
|
||||||
|
</div> :
|
||||||
|
null}
|
||||||
|
<nav className="site-nav">
|
||||||
|
<div className="site-nav-left">
|
||||||
|
{/* The navigation items as setup in Ghost */}
|
||||||
|
<Navigation data={site.navigation} navClass="site-nav-item" />
|
||||||
|
</div>
|
||||||
|
<div className="site-nav-right">
|
||||||
|
<Link className="site-nav-button" to="/about">About</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="site-main">
|
||||||
|
{/* All the main content gets inserted here, index.js, post.js */}
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="viewport-bottom">
|
||||||
|
{/* The footer at the very bottom of the screen */}
|
||||||
|
<footer className="site-foot">
|
||||||
|
<div className="site-foot-nav container">
|
||||||
|
<div className="site-foot-nav-left">
|
||||||
|
<Link to="/">{site.title}</Link> © 2019 — Published with <a className="site-foot-nav-item" href="https://ghost.org" target="_blank" rel="noopener noreferrer">Ghost</a>
|
||||||
|
</div>
|
||||||
|
<div className="site-foot-nav-right">
|
||||||
|
<Navigation data={site.navigation} navClass="site-foot-nav-item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DefaultLayout.propTypes = {
|
||||||
|
children: PropTypes.node.isRequired,
|
||||||
|
bodyClass: PropTypes.string,
|
||||||
|
isHome: PropTypes.bool,
|
||||||
|
data: PropTypes.shape({
|
||||||
|
file: PropTypes.object,
|
||||||
|
allGhostSettings: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultLayoutSettingsQuery = props => (
|
||||||
|
<StaticQuery
|
||||||
|
query={graphql`
|
||||||
|
query GhostSettings {
|
||||||
|
allGhostSettings {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...GhostSettingsFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file(relativePath: {eq: "ghost-icon.png"}) {
|
||||||
|
childImageSharp {
|
||||||
|
fixed(width: 30, height: 30) {
|
||||||
|
...GatsbyImageSharpFixed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
render={data => <DefaultLayout data={data} {...props} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default DefaultLayoutSettingsQuery
|
41
src/components/common/Navigation.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Link } from 'gatsby'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigation component
|
||||||
|
*
|
||||||
|
* The Navigation component takes an array of your Ghost
|
||||||
|
* navigation property that is fetched from the settings.
|
||||||
|
* It differentiates between absolute (external) and relative link (internal).
|
||||||
|
* You can pass it a custom class for your own styles, but it will always fallback
|
||||||
|
* to a `site-nav-item` class.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const Navigation = ({ data, navClass }) => (
|
||||||
|
<>
|
||||||
|
{data.map((navItem, i) => {
|
||||||
|
if (navItem.url.match(/^\s?http(s?)/gi)) {
|
||||||
|
return <a className={navClass} href={navItem.url} key={i} target="_blank" rel="noopener noreferrer">{navItem.label}</a>
|
||||||
|
} else {
|
||||||
|
return <Link className={navClass} to={navItem.url} key={i}>{navItem.label}</Link>
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
Navigation.defaultProps = {
|
||||||
|
navClass: `site-nav-item`,
|
||||||
|
}
|
||||||
|
|
||||||
|
Navigation.propTypes = {
|
||||||
|
data: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
).isRequired,
|
||||||
|
navClass: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navigation
|
36
src/components/common/Pagination.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Link } from 'gatsby'
|
||||||
|
|
||||||
|
const Pagination = ({ pageContext }) => {
|
||||||
|
const { previousPagePath, nextPagePath, humanPageNumber, numberOfPages } = pageContext
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="pagination" role="navigation">
|
||||||
|
<div>
|
||||||
|
{previousPagePath && (
|
||||||
|
|
||||||
|
<Link to={previousPagePath} rel="prev">
|
||||||
|
Previous
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{numberOfPages > 1 && <div className="pagination-location">Page {humanPageNumber} of {numberOfPages}</div>}
|
||||||
|
<div>
|
||||||
|
{nextPagePath && (
|
||||||
|
|
||||||
|
<Link to={nextPagePath} rel="next">
|
||||||
|
Next
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Pagination.propTypes = {
|
||||||
|
pageContext: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Pagination
|
60
src/components/common/PostCard.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { Link } from 'gatsby'
|
||||||
|
import { Tags } from '@tryghost/helpers-gatsby'
|
||||||
|
import { readingTime as readingTimeHelper } from '@tryghost/helpers'
|
||||||
|
|
||||||
|
const PostCard = ({ post }) => {
|
||||||
|
const url = `/${post.slug}/`
|
||||||
|
const readingTime = readingTimeHelper(post)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={url} className="post-card">
|
||||||
|
<header className="post-card-header">
|
||||||
|
{post.feature_image &&
|
||||||
|
<div className="post-card-image" style={{
|
||||||
|
backgroundImage: `url(${post.feature_image})` ,
|
||||||
|
}}></div>}
|
||||||
|
{post.tags && <div className="post-card-tags"> <Tags post={post} visibility="public" autolink={false} /></div>}
|
||||||
|
{post.featured && <span>Featured</span>}
|
||||||
|
<h2 className="post-card-title">{post.title}</h2>
|
||||||
|
</header>
|
||||||
|
<section className="post-card-excerpt">{post.excerpt}</section>
|
||||||
|
<footer className="post-card-footer">
|
||||||
|
<div className="post-card-footer-left">
|
||||||
|
<div className="post-card-avatar">
|
||||||
|
{post.primary_author.profile_image ?
|
||||||
|
<img className="author-profile-image" src={post.primary_author.profile_image} alt={post.primary_author.name}/> :
|
||||||
|
<img className="default-avatar" src="/images/icons/avatar.svg" alt={post.primary_author.name}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<span>{ post.primary_author.name }</span>
|
||||||
|
</div>
|
||||||
|
<div className="post-card-footer-right">
|
||||||
|
<div>{readingTime}</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
PostCard.propTypes = {
|
||||||
|
post: PropTypes.shape({
|
||||||
|
slug: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
feature_image: PropTypes.string,
|
||||||
|
featured: PropTypes.bool,
|
||||||
|
tags: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
excerpt: PropTypes.string.isRequired,
|
||||||
|
primary_author: PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
profile_image: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostCard
|
4
src/components/common/index.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export { default as Layout } from './Layout'
|
||||||
|
export { default as PostCard } from './PostCard'
|
||||||
|
export { default as Pagination } from './Pagination'
|
||||||
|
export { default as Navigation } from './Navigation'
|
170
src/components/common/meta/ArticleMeta.js
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Helmet from "react-helmet"
|
||||||
|
import { StaticQuery, graphql } from 'gatsby'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import url from 'url'
|
||||||
|
|
||||||
|
import getAuthorProperties from './getAuthorProperties'
|
||||||
|
import ImageMeta from './ImageMeta'
|
||||||
|
import config from '../../../utils/siteConfig'
|
||||||
|
|
||||||
|
import { tags as tagsHelper } from '@tryghost/helpers'
|
||||||
|
|
||||||
|
const ArticleMetaGhost = ({ data, settings, canonical }) => {
|
||||||
|
const ghostPost = data
|
||||||
|
settings = settings.allGhostSettings.edges[0].node
|
||||||
|
|
||||||
|
const author = getAuthorProperties(ghostPost.primary_author)
|
||||||
|
const publicTags = _.map(tagsHelper(ghostPost, { visibility: `public`, fn: tag => tag }), `name`)
|
||||||
|
const primaryTag = publicTags[0] || ``
|
||||||
|
const shareImage = ghostPost.feature_image ? ghostPost.feature_image : _.get(settings, `cover_image`, null)
|
||||||
|
const publisherLogo = (settings.logo || config.siteIcon) ? url.resolve(config.siteUrl, (settings.logo || config.siteIcon)) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{ghostPost.meta_title || ghostPost.title}</title>
|
||||||
|
<meta name="description" content={ghostPost.meta_description || ghostPost.excerpt} />
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
|
|
||||||
|
<meta property="og:site_name" content={settings.title} />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:title"
|
||||||
|
content={
|
||||||
|
ghostPost.og_title ||
|
||||||
|
ghostPost.meta_title ||
|
||||||
|
ghostPost.title
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<meta property="og:description"
|
||||||
|
content={
|
||||||
|
ghostPost.og_description ||
|
||||||
|
ghostPost.excerpt ||
|
||||||
|
ghostPost.meta_description
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<meta property="og:url" content={canonical} />
|
||||||
|
<meta property="article:published_time" content={ghostPost.published_at} />
|
||||||
|
<meta property="article:modified_time" content={ghostPost.updated_at} />
|
||||||
|
{publicTags.map((keyword, i) => (<meta property="article:tag" content={keyword} key={i} />))}
|
||||||
|
{author.facebookUrl && <meta property="article:author" content={author.facebookUrl} />}
|
||||||
|
|
||||||
|
<meta name="twitter:title"
|
||||||
|
content={
|
||||||
|
ghostPost.twitter_title ||
|
||||||
|
ghostPost.meta_title ||
|
||||||
|
ghostPost.title
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<meta name="twitter:description"
|
||||||
|
content={
|
||||||
|
ghostPost.twitter_description ||
|
||||||
|
ghostPost.excerpt ||
|
||||||
|
ghostPost.meta_description
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<meta name="twitter:url" content={canonical} />
|
||||||
|
<meta name="twitter:label1" content="Written by" />
|
||||||
|
<meta name="twitter:data1" content={author.name} />
|
||||||
|
{primaryTag && <meta name="twitter:label2" content="Filed under" />}
|
||||||
|
{primaryTag && <meta name="twitter:data2" content={primaryTag} />}
|
||||||
|
|
||||||
|
{settings.twitter && <meta name="twitter:site" content={`https://twitter.com/${settings.twitter.replace(/^@/, ``)}/`} />}
|
||||||
|
{settings.twitter && <meta name="twitter:creator" content={settings.twitter} />}
|
||||||
|
<script type="application/ld+json">{`
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org/",
|
||||||
|
"@type": "Article",
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "${author.name}",
|
||||||
|
${author.image ? author.sameAsArray ? `"image": "${author.image}",` : `"image": "${author.image}"` : ``}
|
||||||
|
${author.sameAsArray ? `"sameAs": ${author.sameAsArray}` : ``}
|
||||||
|
},
|
||||||
|
${publicTags.length ? `"keywords": "${_.join(publicTags, `, `)}",` : ``}
|
||||||
|
"headline": "${ghostPost.meta_title || ghostPost.title}",
|
||||||
|
"url": "${canonical}",
|
||||||
|
"datePublished": "${ghostPost.published_at}",
|
||||||
|
"dateModified": "${ghostPost.updated_at}",
|
||||||
|
${shareImage ? `"image": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "${shareImage}",
|
||||||
|
"width": "${config.shareImageWidth}",
|
||||||
|
"height": "${config.shareImageHeight}"
|
||||||
|
},` : ``}
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "${settings.title}",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "${publisherLogo}",
|
||||||
|
"width": 60,
|
||||||
|
"height": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "${ghostPost.meta_description || ghostPost.excerpt}",
|
||||||
|
"mainEntityOfPage": {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "${config.siteUrl}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</script>
|
||||||
|
</Helmet>
|
||||||
|
<ImageMeta image={shareImage} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ArticleMetaGhost.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
published_at: PropTypes.string.isRequired,
|
||||||
|
updated_at: PropTypes.string.isRequired,
|
||||||
|
meta_title: PropTypes.string,
|
||||||
|
meta_description: PropTypes.string,
|
||||||
|
primary_author: PropTypes.object.isRequired,
|
||||||
|
feature_image: PropTypes.string,
|
||||||
|
tags: PropTypes.arrayOf(
|
||||||
|
PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
slug: PropTypes.string,
|
||||||
|
visibility: PropTypes.string,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
primaryTag: PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
}),
|
||||||
|
og_title: PropTypes.string,
|
||||||
|
og_description: PropTypes.string,
|
||||||
|
twitter_title: PropTypes.string,
|
||||||
|
twitter_description: PropTypes.string,
|
||||||
|
excerpt: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
settings: PropTypes.shape({
|
||||||
|
logo: PropTypes.object,
|
||||||
|
title: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
allGhostSettings: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
canonical: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArticleMetaQuery = props => (
|
||||||
|
<StaticQuery
|
||||||
|
query={graphql`
|
||||||
|
query GhostSettingsArticleMeta {
|
||||||
|
allGhostSettings {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...GhostSettingsFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
render={data => <ArticleMetaGhost settings={data} {...props} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ArticleMetaQuery
|
96
src/components/common/meta/AuthorMeta.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Helmet from 'react-helmet'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { StaticQuery, graphql } from 'gatsby'
|
||||||
|
|
||||||
|
import ImageMeta from './ImageMeta'
|
||||||
|
import getAuthorProperties from './getAuthorProperties'
|
||||||
|
import config from '../../../utils/siteConfig'
|
||||||
|
|
||||||
|
const AuthorMeta = ({ data, settings, canonical }) => {
|
||||||
|
settings = settings.allGhostSettings.edges[0].node
|
||||||
|
|
||||||
|
const author = getAuthorProperties(data)
|
||||||
|
const shareImage = author.image || _.get(settings, `cover_image`, null)
|
||||||
|
const title = `${data.name} - ${settings.title}`
|
||||||
|
const description = data.bio || config.siteDescriptionMeta || settings.description
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
|
<meta property="og:site_name" content={settings.title} />
|
||||||
|
<meta property="og:type" content="profile" />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:url" content={canonical} />
|
||||||
|
<meta name="twitter:title" content={title} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
<meta name="twitter:url" content={canonical} />
|
||||||
|
{settings.twitter && <meta name="twitter:site" content={`https://twitter.com/${settings.twitter.replace(/^@/, ``)}/`} />}
|
||||||
|
{settings.twitter && <meta name="twitter:creator" content={settings.twitter} />}
|
||||||
|
<script type="application/ld+json">{`
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org/",
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "${data.name}",
|
||||||
|
${author.sameAsArray ? `"sameAs": ${author.sameAsArray},` : ``}
|
||||||
|
"url": "${canonical}",
|
||||||
|
${shareImage ? `"image": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "${shareImage}",
|
||||||
|
"width": "${config.shareImageWidth}",
|
||||||
|
"height": "${config.shareImageHeight}"
|
||||||
|
},` : ``}
|
||||||
|
"mainEntityOfPage": {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "${config.siteUrl}"
|
||||||
|
},
|
||||||
|
"description": "${description}"
|
||||||
|
}
|
||||||
|
`}</script>
|
||||||
|
</Helmet>
|
||||||
|
<ImageMeta image={shareImage} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorMeta.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
bio: PropTypes.string,
|
||||||
|
profile_image: PropTypes.string,
|
||||||
|
website: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
facebook: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
settings: PropTypes.shape({
|
||||||
|
title: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
allGhostSettings: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
canonical: PropTypes.string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthorMetaQuery = props => (
|
||||||
|
<StaticQuery
|
||||||
|
query={graphql`
|
||||||
|
query GhostSettingsAuthorMeta {
|
||||||
|
allGhostSettings {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...GhostSettingsFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
render={data => <AuthorMeta settings={data} {...props} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default AuthorMetaQuery
|
26
src/components/common/meta/ImageMeta.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Helmet from 'react-helmet'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import config from '../../../utils/siteConfig'
|
||||||
|
|
||||||
|
const ImageMeta = ({ image }) => {
|
||||||
|
if (!image) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Helmet>
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:image" content={image} />
|
||||||
|
<meta property="og:image" content={image} />
|
||||||
|
<meta property="og:image:width" content={config.shareImageWidth} />
|
||||||
|
<meta property="og:image:height" content={config.shareImageHeight} />
|
||||||
|
</Helmet >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageMeta.propTypes = {
|
||||||
|
image: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageMeta
|
118
src/components/common/meta/MetaData.js
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { StaticQuery, graphql } from 'gatsby'
|
||||||
|
import url from 'url'
|
||||||
|
|
||||||
|
import config from '../../../utils/siteConfig'
|
||||||
|
import ArticleMeta from './ArticleMeta'
|
||||||
|
import WebsiteMeta from './WebsiteMeta'
|
||||||
|
import AuthorMeta from './AuthorMeta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetaData will generate all relevant meta data information incl.
|
||||||
|
* JSON-LD (schema.org), Open Graph (Facebook) and Twitter properties.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const MetaData = ({
|
||||||
|
data,
|
||||||
|
settings,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
image,
|
||||||
|
location,
|
||||||
|
}) => {
|
||||||
|
const canonical = url.resolve(config.siteUrl, location.pathname)
|
||||||
|
const { ghostPost, ghostTag, ghostAuthor, ghostPage } = data
|
||||||
|
settings = settings.allGhostSettings.edges[0].node
|
||||||
|
|
||||||
|
if (ghostPost) {
|
||||||
|
return (
|
||||||
|
<ArticleMeta
|
||||||
|
data={ghostPost}
|
||||||
|
canonical={canonical}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (ghostTag) {
|
||||||
|
return (
|
||||||
|
<WebsiteMeta
|
||||||
|
data={ghostTag}
|
||||||
|
canonical={canonical}
|
||||||
|
type="Series"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (ghostAuthor) {
|
||||||
|
return (
|
||||||
|
<AuthorMeta
|
||||||
|
data={ghostAuthor}
|
||||||
|
canonical={canonical}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else if (ghostPage) {
|
||||||
|
return (
|
||||||
|
<WebsiteMeta
|
||||||
|
data={ghostPage}
|
||||||
|
canonical={canonical}
|
||||||
|
type="WebSite"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
title = title || config.siteTitleMeta || settings.title
|
||||||
|
description = description || config.siteDescriptionMeta || settings.description
|
||||||
|
image = image || settings.cover_image || null
|
||||||
|
|
||||||
|
image = image ? url.resolve(config.siteUrl, image) : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebsiteMeta
|
||||||
|
data={{}}
|
||||||
|
canonical={canonical}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
image={image}
|
||||||
|
type="WebSite"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MetaData.defaultProps = {
|
||||||
|
data: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
MetaData.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
ghostPost: PropTypes.object,
|
||||||
|
ghostTag: PropTypes.object,
|
||||||
|
ghostAuthor: PropTypes.object,
|
||||||
|
ghostPage: PropTypes.object,
|
||||||
|
}).isRequired,
|
||||||
|
settings: PropTypes.shape({
|
||||||
|
allGhostSettings: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
title: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
image: PropTypes.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetaDataQuery = props => (
|
||||||
|
<StaticQuery
|
||||||
|
query={graphql`
|
||||||
|
query GhostSettingsMetaData {
|
||||||
|
allGhostSettings {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
render={data => <MetaData settings={data} {...props} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default MetaDataQuery
|
114
src/components/common/meta/WebsiteMeta.js
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Helmet from "react-helmet"
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import _ from 'lodash'
|
||||||
|
import { StaticQuery, graphql } from 'gatsby'
|
||||||
|
import url from 'url'
|
||||||
|
|
||||||
|
import ImageMeta from './ImageMeta'
|
||||||
|
import config from '../../../utils/siteConfig'
|
||||||
|
|
||||||
|
const WebsiteMeta = ({ data, settings, canonical, title, description, image, type }) => {
|
||||||
|
settings = settings.allGhostSettings.edges[0].node
|
||||||
|
|
||||||
|
const publisherLogo = url.resolve(config.siteUrl, (settings.logo || config.siteIcon))
|
||||||
|
let shareImage = image || data.feature_image || _.get(settings, `cover_image`, null)
|
||||||
|
|
||||||
|
shareImage = shareImage ? url.resolve(config.siteUrl, shareImage) : null
|
||||||
|
|
||||||
|
description = description || data.meta_description || data.description || config.siteDescriptionMeta || settings.description
|
||||||
|
title = `${title || data.meta_title || data.name || data.title} - ${settings.title}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
|
<meta property="og:site_name" content={settings.title} />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:url" content={canonical} />
|
||||||
|
<meta name="twitter:title" content={title} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
<meta name="twitter:url" content={canonical} />
|
||||||
|
{settings.twitter && <meta name="twitter:site" content={`https://twitter.com/${settings.twitter.replace(/^@/, ``)}/`} />}
|
||||||
|
{settings.twitter && <meta name="twitter:creator" content={settings.twitter} />}
|
||||||
|
<script type="application/ld+json">{`
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org/",
|
||||||
|
"@type": "${type}",
|
||||||
|
"url": "${canonical}",
|
||||||
|
${shareImage ? `"image": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "${shareImage}",
|
||||||
|
"width": "${config.shareImageWidth}",
|
||||||
|
"height": "${config.shareImageHeight}"
|
||||||
|
},` : ``}
|
||||||
|
"publisher": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "${settings.title}",
|
||||||
|
"logo": {
|
||||||
|
"@type": "ImageObject",
|
||||||
|
"url": "${publisherLogo}",
|
||||||
|
"width": 60,
|
||||||
|
"height": 60
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mainEntityOfPage": {
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "${config.siteUrl}"
|
||||||
|
},
|
||||||
|
"description": "${description}"
|
||||||
|
}
|
||||||
|
`}</script>
|
||||||
|
</Helmet>
|
||||||
|
<ImageMeta image={shareImage} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
WebsiteMeta.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
title: PropTypes.string,
|
||||||
|
meta_title: PropTypes.string,
|
||||||
|
meta_description: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
|
feature_image: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
bio: PropTypes.string,
|
||||||
|
profile_image: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
settings: PropTypes.shape({
|
||||||
|
logo: PropTypes.object,
|
||||||
|
description: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
allGhostSettings: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
canonical: PropTypes.string.isRequired,
|
||||||
|
title: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
image: PropTypes.string,
|
||||||
|
type: PropTypes.oneOf([`WebSite`, `Series`]).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
const WebsiteMetaQuery = props => (
|
||||||
|
<StaticQuery
|
||||||
|
query={graphql`
|
||||||
|
query GhostSettingsWebsiteMeta {
|
||||||
|
allGhostSettings {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...GhostSettingsFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
render={data => <WebsiteMeta settings={data} {...props} />}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default WebsiteMetaQuery
|
37
src/components/common/meta/getAuthorProperties.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
|
export const getAuthorProperties = (primaryAuthor) => {
|
||||||
|
let authorProfiles = []
|
||||||
|
|
||||||
|
authorProfiles.push(
|
||||||
|
primaryAuthor.website ? primaryAuthor.website : null,
|
||||||
|
primaryAuthor.twitter ? `https://twitter.com/${primaryAuthor.twitter.replace(/^@/, ``)}/` : null,
|
||||||
|
primaryAuthor.facebook ? `https://www.facebook.com/${primaryAuthor.facebook.replace(/^\//, ``)}/` : null
|
||||||
|
)
|
||||||
|
|
||||||
|
authorProfiles = _.compact(authorProfiles)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: primaryAuthor.name || null,
|
||||||
|
sameAsArray: authorProfiles.length ? `["${_.join(authorProfiles, `", "`)}"]` : null,
|
||||||
|
image: primaryAuthor.profile_image || null,
|
||||||
|
facebookUrl: primaryAuthor.facebook ? `https://www.facebook.com/${primaryAuthor.facebook.replace(/^\//, ``)}/` : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorProperties.defaultProps = {
|
||||||
|
fetchAuthorData: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
getAuthorProperties.PropTypes = {
|
||||||
|
primaryAuthor: PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
profile_image: PropTypes.string,
|
||||||
|
website: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
facebook: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getAuthorProperties
|
1
src/components/common/meta/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default as MetaData } from './MetaData'
|
BIN
src/images/ghost-icon.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
18
src/pages/404.js
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Link } from 'gatsby'
|
||||||
|
import { Layout } from '../components/common'
|
||||||
|
|
||||||
|
const NotFoundPage = () => (
|
||||||
|
<Layout>
|
||||||
|
<div className="container">
|
||||||
|
<article className="content" style={{ textAlign: `center` }}>
|
||||||
|
<h1 className="content-title">Error 404</h1>
|
||||||
|
<section className="content-body">
|
||||||
|
Page not found, <Link to="/">return home</Link> to start over
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default NotFoundPage
|
1015
src/styles/app.css
Normal file
96
src/templates/author.js
Executable file
|
@ -0,0 +1,96 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { graphql } from 'gatsby'
|
||||||
|
|
||||||
|
import { Layout, PostCard, Pagination } from '../components/common'
|
||||||
|
import { MetaData } from '../components/common/meta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Author page (/author/:slug)
|
||||||
|
*
|
||||||
|
* Loads all posts for the requested author incl. pagination.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const Author = ({ data, location, pageContext }) => {
|
||||||
|
const author = data.ghostAuthor
|
||||||
|
const posts = data.allGhostPost.edges
|
||||||
|
const twitterUrl = author.twitter ? `https://twitter.com/${author.twitter.replace(/^@/, ``)}` : null
|
||||||
|
const facebookUrl = author.facebook ? `https://www.facebook.com/${author.facebook.replace(/^\//, ``)}` : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MetaData
|
||||||
|
data={data}
|
||||||
|
location={location}
|
||||||
|
type="profile"
|
||||||
|
/>
|
||||||
|
<Layout>
|
||||||
|
<div className="container">
|
||||||
|
<header className="author-header">
|
||||||
|
<div className="author-header-content">
|
||||||
|
<h1>{author.name}</h1>
|
||||||
|
{author.bio && <p>{author.bio}</p>}
|
||||||
|
<div className="author-header-meta">
|
||||||
|
{author.website && <a className="author-header-item" href={author.website} target="_blank" rel="noopener noreferrer">Website</a>}
|
||||||
|
{twitterUrl && <a className="author-header-item" href={twitterUrl} target="_blank" rel="noopener noreferrer">Twitter</a>}
|
||||||
|
{facebookUrl && <a className="author-header-item" href={facebookUrl} target="_blank" rel="noopener noreferrer">Facebook</a>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="author-header-image">
|
||||||
|
{author.profile_image && <img src={author.profile_image} alt={author.name} />}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section className="post-feed">
|
||||||
|
{posts.map(({ node }) => (
|
||||||
|
// The tag below includes the markup for each post - components/common/PostCard.js
|
||||||
|
<PostCard key={node.id} post={node} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
<Pagination pageContext={pageContext} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Author.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
ghostAuthor: PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
cover_image: PropTypes.string,
|
||||||
|
profile_image: PropTypes.string,
|
||||||
|
website: PropTypes.string,
|
||||||
|
bio: PropTypes.string,
|
||||||
|
location: PropTypes.string,
|
||||||
|
facebook: PropTypes.string,
|
||||||
|
twitter: PropTypes.string,
|
||||||
|
}),
|
||||||
|
allGhostPost: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
pageContext: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Author
|
||||||
|
|
||||||
|
export const pageQuery = graphql`
|
||||||
|
query GhostAuthorQuery($slug: String!, $limit: Int!, $skip: Int!) {
|
||||||
|
ghostAuthor(slug: { eq: $slug }) {
|
||||||
|
...GhostAuthorFields
|
||||||
|
}
|
||||||
|
allGhostPost(
|
||||||
|
sort: { order: DESC, fields: [published_at] },
|
||||||
|
filter: {authors: {elemMatch: {slug: {eq: $slug}}}},
|
||||||
|
limit: $limit,
|
||||||
|
skip: $skip
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...GhostPostFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
65
src/templates/index.js
Executable file
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { graphql } from 'gatsby'
|
||||||
|
|
||||||
|
import { Layout, PostCard, Pagination } from '../components/common'
|
||||||
|
import { MetaData } from '../components/common/meta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main index page (home page)
|
||||||
|
*
|
||||||
|
* Loads all posts from Ghost and uses pagination to navigate through them.
|
||||||
|
* The number of posts that should appear per page can be setup
|
||||||
|
* in /utils/siteConfig.js under `postsPerPage`.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const Index = ({ data, location, pageContext }) => {
|
||||||
|
const posts = data.allGhostPost.edges
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MetaData location={location} />
|
||||||
|
<Layout isHome={true}>
|
||||||
|
<div className="container">
|
||||||
|
<section className="post-feed">
|
||||||
|
{posts.map(({ node }) => (
|
||||||
|
// The tag below includes the markup for each post - components/common/PostCard.js
|
||||||
|
<PostCard key={node.id} post={node} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
<Pagination pageContext={pageContext} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Index.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
allGhostPost: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
pageContext: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index
|
||||||
|
|
||||||
|
// This page query loads all posts sorted descending by published date
|
||||||
|
// The `limit` and `skip` values are used for pagination
|
||||||
|
export const pageQuery = graphql`
|
||||||
|
query GhostPostQuery($limit: Int!, $skip: Int!) {
|
||||||
|
allGhostPost(
|
||||||
|
sort: { order: DESC, fields: [published_at] },
|
||||||
|
limit: $limit,
|
||||||
|
skip: $skip
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...GhostPostFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
65
src/templates/page.js
Executable file
|
@ -0,0 +1,65 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { graphql } from 'gatsby'
|
||||||
|
import Helmet from 'react-helmet'
|
||||||
|
|
||||||
|
import { Layout } from '../components/common'
|
||||||
|
import { MetaData } from '../components/common/meta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single page (/:slug)
|
||||||
|
*
|
||||||
|
* This file renders a single page and loads all the content.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const Page = ({ data, location }) => {
|
||||||
|
const page = data.ghostPage
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MetaData
|
||||||
|
data={data}
|
||||||
|
location={location}
|
||||||
|
type="website"
|
||||||
|
/>
|
||||||
|
<Helmet>
|
||||||
|
<style type="text/css">{`${page.codeinjection_styles}`}</style>
|
||||||
|
</Helmet>
|
||||||
|
<Layout>
|
||||||
|
<div className="container">
|
||||||
|
<article className="content">
|
||||||
|
<h1 className="content-title">{page.title}</h1>
|
||||||
|
|
||||||
|
{/* The main page content */}
|
||||||
|
<section
|
||||||
|
className="content-body load-external-scripts"
|
||||||
|
dangerouslySetInnerHTML={{ __html: page.html }}
|
||||||
|
/>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Page.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
ghostPage: PropTypes.shape({
|
||||||
|
codeinjection_styles: PropTypes.object,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
html: PropTypes.string.isRequired,
|
||||||
|
feature_image: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
location: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Page
|
||||||
|
|
||||||
|
export const postQuery = graphql`
|
||||||
|
query($slug: String!) {
|
||||||
|
ghostPage(slug: { eq: $slug }) {
|
||||||
|
...GhostPageFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
71
src/templates/post.js
Executable file
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { graphql } from 'gatsby'
|
||||||
|
import Helmet from 'react-helmet'
|
||||||
|
|
||||||
|
import { Layout } from '../components/common'
|
||||||
|
import { MetaData } from '../components/common/meta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single post view (/:slug)
|
||||||
|
*
|
||||||
|
* This file renders a single post and loads all the content.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const Post = ({ data, location }) => {
|
||||||
|
const post = data.ghostPost
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MetaData
|
||||||
|
data={data}
|
||||||
|
location={location}
|
||||||
|
type="article"
|
||||||
|
/>
|
||||||
|
<Helmet>
|
||||||
|
<style type="text/css">{`${post.codeinjection_styles}`}</style>
|
||||||
|
</Helmet>
|
||||||
|
<Layout>
|
||||||
|
<div className="container">
|
||||||
|
<article className="content">
|
||||||
|
{ post.feature_image ?
|
||||||
|
<figure className="post-feature-image">
|
||||||
|
<img src={ post.feature_image } alt={ post.title } />
|
||||||
|
</figure> : null }
|
||||||
|
<section className="post-full-content">
|
||||||
|
<h1 className="content-title">{post.title}</h1>
|
||||||
|
|
||||||
|
{/* The main post content */ }
|
||||||
|
<section
|
||||||
|
className="content-body load-external-scripts"
|
||||||
|
dangerouslySetInnerHTML={{ __html: post.html }}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Post.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
ghostPost: PropTypes.shape({
|
||||||
|
codeinjection_styles: PropTypes.object,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
html: PropTypes.string.isRequired,
|
||||||
|
feature_image: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
location: PropTypes.object.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Post
|
||||||
|
|
||||||
|
export const postQuery = graphql`
|
||||||
|
query($slug: String!) {
|
||||||
|
ghostPost(slug: { eq: $slug }) {
|
||||||
|
...GhostPostFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
78
src/templates/tag.js
Executable file
|
@ -0,0 +1,78 @@
|
||||||
|
import React from 'react'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import { graphql } from 'gatsby'
|
||||||
|
|
||||||
|
import { Layout, PostCard, Pagination } from '../components/common'
|
||||||
|
import { MetaData } from '../components/common/meta'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag page (/tag/:slug)
|
||||||
|
*
|
||||||
|
* Loads all posts for the requested tag incl. pagination.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const Tag = ({ data, location, pageContext }) => {
|
||||||
|
const tag = data.ghostTag
|
||||||
|
const posts = data.allGhostPost.edges
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MetaData
|
||||||
|
data={data}
|
||||||
|
location={location}
|
||||||
|
type="series"
|
||||||
|
/>
|
||||||
|
<Layout>
|
||||||
|
<div className="container">
|
||||||
|
<header className="tag-header">
|
||||||
|
<h1>{tag.name}</h1>
|
||||||
|
{tag.description ? <p>{tag.description}</p> : null }
|
||||||
|
</header>
|
||||||
|
<section className="post-feed">
|
||||||
|
{posts.map(({ node }) => (
|
||||||
|
// The tag below includes the markup for each post - components/common/PostCard.js
|
||||||
|
<PostCard key={node.id} post={node} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
<Pagination pageContext={pageContext} />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag.propTypes = {
|
||||||
|
data: PropTypes.shape({
|
||||||
|
ghostTag: PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string,
|
||||||
|
}),
|
||||||
|
allGhostPost: PropTypes.object.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
pageContext: PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tag
|
||||||
|
|
||||||
|
export const pageQuery = graphql`
|
||||||
|
query GhostTagQuery($slug: String!, $limit: Int!, $skip: Int!) {
|
||||||
|
ghostTag(slug: { eq: $slug }) {
|
||||||
|
...GhostTagFields
|
||||||
|
}
|
||||||
|
allGhostPost(
|
||||||
|
sort: { order: DESC, fields: [published_at] },
|
||||||
|
filter: {tags: {elemMatch: {slug: {eq: $slug}}}},
|
||||||
|
limit: $limit,
|
||||||
|
skip: $skip
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...GhostPostFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
239
src/utils/fragments.js
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
import { graphql } from 'gatsby'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These so called fragments are the fields we query on each template.
|
||||||
|
* A fragment make queries a bit more reuseable, so instead of typing and
|
||||||
|
* remembering every possible field, you can just use
|
||||||
|
* ...GhostPostFields
|
||||||
|
* for example to load all post fields into your GraphQL query.
|
||||||
|
*
|
||||||
|
* Further info 👉🏼 https://www.gatsbyjs.org/docs/graphql-reference/#fragments
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Used for tag archive pages
|
||||||
|
export const ghostTagFields = graphql`
|
||||||
|
fragment GhostTagFields on GhostTag {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
visibility
|
||||||
|
feature_image
|
||||||
|
description
|
||||||
|
meta_title
|
||||||
|
meta_description
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Used for author pages
|
||||||
|
export const ghostAuthorFields = graphql`
|
||||||
|
fragment GhostAuthorFields on GhostAuthor {
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
bio
|
||||||
|
cover_image
|
||||||
|
profile_image
|
||||||
|
location
|
||||||
|
website
|
||||||
|
twitter
|
||||||
|
facebook
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Used for single posts
|
||||||
|
export const ghostPostFields = graphql`
|
||||||
|
fragment GhostPostFields on GhostPost {
|
||||||
|
# Main fields
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
featured
|
||||||
|
feature_image
|
||||||
|
excerpt
|
||||||
|
custom_excerpt
|
||||||
|
|
||||||
|
# Dates formatted
|
||||||
|
created_at_pretty: created_at(formatString: "DD MMMM, YYYY")
|
||||||
|
published_at_pretty: published_at(formatString: "DD MMMM, YYYY")
|
||||||
|
updated_at_pretty: updated_at(formatString: "DD MMMM, YYYY")
|
||||||
|
|
||||||
|
# Dates unformatted
|
||||||
|
created_at
|
||||||
|
published_at
|
||||||
|
updated_at
|
||||||
|
|
||||||
|
# SEO
|
||||||
|
meta_title
|
||||||
|
meta_description
|
||||||
|
og_description
|
||||||
|
og_image
|
||||||
|
og_title
|
||||||
|
twitter_description
|
||||||
|
twitter_image
|
||||||
|
twitter_title
|
||||||
|
|
||||||
|
# Authors
|
||||||
|
authors {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
bio
|
||||||
|
# email
|
||||||
|
profile_image
|
||||||
|
twitter
|
||||||
|
facebook
|
||||||
|
website
|
||||||
|
}
|
||||||
|
primary_author {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
bio
|
||||||
|
# email
|
||||||
|
profile_image
|
||||||
|
twitter
|
||||||
|
facebook
|
||||||
|
website
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
primary_tag {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
feature_image
|
||||||
|
meta_description
|
||||||
|
meta_title
|
||||||
|
visibility
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
feature_image
|
||||||
|
meta_description
|
||||||
|
meta_title
|
||||||
|
visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
# Content
|
||||||
|
plaintext
|
||||||
|
html
|
||||||
|
|
||||||
|
# Additional fields
|
||||||
|
url
|
||||||
|
uuid
|
||||||
|
page
|
||||||
|
codeinjection_foot
|
||||||
|
codeinjection_head
|
||||||
|
codeinjection_styles
|
||||||
|
comment_id
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Used for single pages
|
||||||
|
export const ghostPageFields = graphql`
|
||||||
|
fragment GhostPageFields on GhostPage {
|
||||||
|
# Main fields
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
featured
|
||||||
|
feature_image
|
||||||
|
excerpt
|
||||||
|
custom_excerpt
|
||||||
|
|
||||||
|
# Dates formatted
|
||||||
|
created_at_pretty: created_at(formatString: "DD MMMM, YYYY")
|
||||||
|
published_at_pretty: published_at(formatString: "DD MMMM, YYYY")
|
||||||
|
updated_at_pretty: updated_at(formatString: "DD MMMM, YYYY")
|
||||||
|
|
||||||
|
# Dates unformatted
|
||||||
|
created_at
|
||||||
|
published_at
|
||||||
|
updated_at
|
||||||
|
|
||||||
|
# SEO
|
||||||
|
meta_title
|
||||||
|
meta_description
|
||||||
|
og_description
|
||||||
|
og_image
|
||||||
|
og_title
|
||||||
|
twitter_description
|
||||||
|
twitter_image
|
||||||
|
twitter_title
|
||||||
|
|
||||||
|
# Authors
|
||||||
|
authors {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
bio
|
||||||
|
# email
|
||||||
|
profile_image
|
||||||
|
twitter
|
||||||
|
facebook
|
||||||
|
website
|
||||||
|
}
|
||||||
|
primary_author {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
bio
|
||||||
|
# email
|
||||||
|
profile_image
|
||||||
|
twitter
|
||||||
|
facebook
|
||||||
|
website
|
||||||
|
}
|
||||||
|
|
||||||
|
# Tags
|
||||||
|
primary_tag {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
feature_image
|
||||||
|
meta_description
|
||||||
|
meta_title
|
||||||
|
visibility
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
description
|
||||||
|
feature_image
|
||||||
|
meta_description
|
||||||
|
meta_title
|
||||||
|
visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
# Content
|
||||||
|
plaintext
|
||||||
|
html
|
||||||
|
|
||||||
|
# Additional fields
|
||||||
|
url
|
||||||
|
uuid
|
||||||
|
page
|
||||||
|
codeinjection_foot
|
||||||
|
codeinjection_head
|
||||||
|
codeinjection_styles
|
||||||
|
comment_id
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// Used for settings
|
||||||
|
export const ghostSettingsFields = graphql`
|
||||||
|
fragment GhostSettingsFields on GhostSettings {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
logo
|
||||||
|
icon
|
||||||
|
cover_image
|
||||||
|
facebook
|
||||||
|
twitter
|
||||||
|
lang
|
||||||
|
timezone
|
||||||
|
codeinjection_head
|
||||||
|
codeinjection_foot
|
||||||
|
codeinjection_styles
|
||||||
|
navigation {
|
||||||
|
label
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
121
src/utils/rss/generate-feed.js
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
const cheerio = require(`cheerio`)
|
||||||
|
const tagsHelper = require(`@tryghost/helpers`).tags
|
||||||
|
const _ = require(`lodash`)
|
||||||
|
|
||||||
|
const generateItem = function generateItem(post) {
|
||||||
|
const itemUrl = post.url
|
||||||
|
const html = post.html
|
||||||
|
const htmlContent = cheerio.load(html, { decodeEntities: false, xmlMode: true })
|
||||||
|
const item = {
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
guid: post.id,
|
||||||
|
url: itemUrl,
|
||||||
|
date: post.published_at,
|
||||||
|
categories: _.map(tagsHelper(post, { visibility: `public`, fn: tag => tag }), `name`),
|
||||||
|
author: post.primary_author ? post.primary_author.name : null,
|
||||||
|
custom_elements: [],
|
||||||
|
}
|
||||||
|
let imageUrl
|
||||||
|
|
||||||
|
if (post.feature_image) {
|
||||||
|
imageUrl = post.feature_image
|
||||||
|
|
||||||
|
// Add a media content tag
|
||||||
|
item.custom_elements.push({
|
||||||
|
'media:content': {
|
||||||
|
_attr: {
|
||||||
|
url: imageUrl,
|
||||||
|
medium: `image`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Also add the image to the content, because not all readers support media:content
|
||||||
|
htmlContent(`p`).first().before(`<img src="` + imageUrl + `" />`)
|
||||||
|
htmlContent(`img`).attr(`alt`, post.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
item.custom_elements.push({
|
||||||
|
'content:encoded': {
|
||||||
|
_cdata: htmlContent.html(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateRSSFeed = function generateRSSFeed(siteConfig) {
|
||||||
|
return {
|
||||||
|
serialize: ({ query: { allGhostPost } }) => allGhostPost.edges.map(edge => Object.assign({}, generateItem(edge.node))),
|
||||||
|
setup: ({ query: { allGhostSettings } }) => {
|
||||||
|
const siteTitle = allGhostSettings.edges[0].node.title || `No Title`
|
||||||
|
const siteDescription = allGhostSettings.edges[0].node.description || `No Description`
|
||||||
|
const feed = {
|
||||||
|
title: siteTitle,
|
||||||
|
description: siteDescription,
|
||||||
|
// generator: `Ghost ` + data.safeVersion,
|
||||||
|
generator: `Ghost 2.9`,
|
||||||
|
feed_url: `${siteConfig.siteUrl}/rss/`,
|
||||||
|
site_url: `${siteConfig.siteUrl}/`,
|
||||||
|
image_url: `${siteConfig.siteUrl}/${siteConfig.siteIcon}`,
|
||||||
|
ttl: `60`,
|
||||||
|
custom_namespaces: {
|
||||||
|
content: `http://purl.org/rss/1.0/modules/content/`,
|
||||||
|
media: `http://search.yahoo.com/mrss/`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...feed,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
query: `
|
||||||
|
{
|
||||||
|
allGhostPost(
|
||||||
|
sort: {order: DESC, fields: published_at}
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
# Main fields
|
||||||
|
id
|
||||||
|
title
|
||||||
|
slug
|
||||||
|
featured
|
||||||
|
feature_image
|
||||||
|
|
||||||
|
# Dates unformatted
|
||||||
|
created_at
|
||||||
|
published_at
|
||||||
|
updated_at
|
||||||
|
|
||||||
|
# SEO
|
||||||
|
excerpt
|
||||||
|
meta_title
|
||||||
|
meta_description
|
||||||
|
|
||||||
|
# Authors
|
||||||
|
authors {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
primary_author {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
# Content
|
||||||
|
html
|
||||||
|
|
||||||
|
# Additional fields
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
output: `/rss`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = generateRSSFeed
|
16
src/utils/siteConfig.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module.exports = {
|
||||||
|
siteUrl: `https://gatsby.ghost.org`, // Site domain. Do not include a trailing slash!
|
||||||
|
|
||||||
|
postsPerPage: 12, // Number of posts shown on paginated pages (changes this requires sometimes to delete the cache)
|
||||||
|
|
||||||
|
siteTitleMeta: `Ghost Gatsby Starter`, // This allows an alternative site title for meta data for pages.
|
||||||
|
siteDescriptionMeta: `A starter template to build amazing static websites with Ghost and Gatsby`, // This allows an alternative site description for meta data for pages.
|
||||||
|
|
||||||
|
shareImageWidth: 1000, // Change to the width of your default share image
|
||||||
|
shareImageHeight: 523, // Change to the height of your default share image
|
||||||
|
|
||||||
|
shortTitle: `Ghost`, // Used for App manifest e.g. Mobile Home Screen
|
||||||
|
siteIcon: `favicon.png`, // Logo in /static dir used for SEO, RSS, and App manifest
|
||||||
|
backgroundColor: `#e9e9e9`, // Used for Offline Manifest
|
||||||
|
themeColor: `#15171A`, // Used for Offline Manifest
|
||||||
|
}
|
18
static/_headers
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# This is recommended to prevent the caching of the service worker file itself
|
||||||
|
# https://www.netlify.com/blog/2018/06/28/5-pro-tips-and-plugins-for-optimizing-your-gatsby---netlify-site/#4-get-your-service-workers-um-working
|
||||||
|
/sw.js # Gatsby's default service worker file path
|
||||||
|
Cache-Control: no-cache
|
||||||
|
|
||||||
|
/rss
|
||||||
|
content-type: application/rss+xml; charset=UTF-8
|
||||||
|
|
||||||
|
# These are default recommended security headers, for Netlify
|
||||||
|
# https://www.netlify.com/docs/headers-and-basic-auth/
|
||||||
|
|
||||||
|
/*
|
||||||
|
Referrer-Policy: no-referrer-when-downgrade
|
||||||
|
Strict-Transport-Security: max-age=31536000
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
X-Frame-Options: SAMEORIGIN
|
||||||
|
X-Xss-Protection: 1; mode=block
|
||||||
|
Feature-Policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'
|
5
static/_redirects
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Netlify redirects file, edit with your domain
|
||||||
|
# https://www.netlify.com/docs/redirects/
|
||||||
|
|
||||||
|
https://gatsby-starter-ghost.netlify.com/* https://gatsby.ghost.org/:splat 301!
|
||||||
|
http://gatsby-starter-ghost.netlify.com/* https://gatsby.ghost.org/:splat 301!
|
BIN
static/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/favicon.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
1
static/images/icons/avatar.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="26" height="26" viewBox="0 0 26 26" xmlns="http://www.w3.org/2000/svg"><title>Group 8</title><g fill="none" fill-rule="evenodd"><path stroke-opacity=".012" stroke="#000" stroke-width="0" d="M1 1h24v24H1z"/><g stroke="#738A94" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2"><path d="M24.5 13c0 3.176-1.123 5.886-3.368 8.132C18.886 23.377 16.176 24.5 13 24.5c-3.176 0-5.886-1.123-8.132-3.368C2.623 18.886 1.5 16.176 1.5 13c0-3.176 1.123-5.886 3.368-8.132C7.114 2.623 9.824 1.5 13 1.5c3.176 0 5.886 1.123 8.132 3.368C23.377 7.114 24.5 9.824 24.5 13z"/><path d="M17.25 9.75c0 1.174-.415 2.175-1.245 3.005C15.175 13.585 14.174 14 13 14s-2.175-.415-3.005-1.245c-.83-.83-1.245-1.831-1.245-3.005s.415-2.175 1.245-3.005C10.825 5.915 11.826 5.5 13 5.5s2.175.415 3.005 1.245c.83.83 1.245 1.831 1.245 3.005zM19.317 19.5c-1.261-2.667-3.367-4-6.317-4s-5.056 1.333-6.317 4"/></g></g></svg>
|
After Width: | Height: | Size: 905 B |
1
static/images/icons/facebook.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#fff" d="M452 0H60C26.916 0 0 26.916 0 60v392c0 33.084 26.916 60 60 60h392c33.084 0 60-26.916 60-60V60c0-33.084-26.916-60-60-60zm20 452c0 11.028-8.972 20-20 20H338V309h61.79L410 247h-72v-43c0-16.975 13.025-30 30-30h41v-62h-41c-50.923 0-91.978 41.25-91.978 92.174V247H216v62h60.022v163H60c-11.028 0-20-8.972-20-20V60c0-11.028 8.972-20 20-20h392c11.028 0 20 8.972 20 20v392z"/></svg>
|
After Width: | Height: | Size: 456 B |
1
static/images/icons/rss.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="614.363" height="614.363"><path fill="#fff" d="M93.299 613.7c-50.798 0-92.117-41.348-92.117-92.117 0-50.827 41.319-92.174 92.117-92.174s92.117 41.376 92.117 92.174c.028 50.77-41.319 92.117-92.117 92.117zm0-155.507c-34.894 0-63.304 28.439-63.304 63.39 0 34.922 28.41 63.304 63.304 63.304 34.893 0 63.303-28.382 63.303-63.304.029-34.95-28.381-63.39-63.303-63.39z"/><path fill="#fff" d="M400.537 614.306h-112.2c-7.952 0-14.407-6.454-14.407-14.407 0-69.585-26.883-134.905-75.664-183.945C149.514 366.998 84.654 340 15.704 340c-7.953 0-14.407-6.454-14.407-14.406V213.336c0-7.953 6.454-14.407 14.407-14.407 220.165 0 399.27 179.883 399.27 400.969-.03 7.982-6.455 14.408-14.437 14.408zm-98.139-28.814h83.473c-7.376-193.743-162.912-349.999-355.79-357.461v83.531c71.285 3.486 137.787 33.021 188.585 84.078 50.828 51.086 80.246 118.048 83.732 189.852z"/><path fill="#fff" d="M598.775 614.363H486.604c-7.952 0-14.406-6.454-14.406-14.407 0-253.04-204.721-458.942-456.378-458.942-7.953 0-14.407-6.454-14.407-14.407v-112.2C1.412 6.455 7.866 0 15.819 0c329.397 0 597.363 269.147 597.363 599.956a14.375 14.375 0 0 1-14.407 14.407zm-97.966-28.813h83.387C576.675 282.085 332.307 36.593 30.226 28.987v83.444c256.124 7.549 463.12 215.64 470.583 473.119z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
static/images/icons/twitter.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="612.001" height="612.002"><path fill="#fff" d="M197.553 557.219c-68.018 0-134.159-19.371-191.294-56.047-5.333-3.428-7.591-10.066-5.469-16.051 2.122-5.986 8.189-9.795 14.42-8.896 47.912 5.576 97.919-4.164 139.873-28.16-41.491-12.461-75.228-44.594-88.859-87.199a13.57 13.57 0 0 1 2.802-13.195 13.736 13.736 0 0 1 7.346-4.244c-32.404-24.678-52.864-63.639-52.864-106.436 0-4.816 2.558-10.012 6.72-12.461 4.163-2.449 9.305-3.21 13.495-.925a67.034 67.034 0 0 0 3.646 1.932c-14.719-21.739-22.881-47.749-22.881-74.902 0-23.562 6.23-46.796 18.093-67.147 2.231-3.863 6.23-6.366 10.692-6.72a13.734 13.734 0 0 1 11.59 4.952c55.04 67.501 134.703 110.298 220.624 119.086a135.305 135.305 0 0 1-.571-12.434c0-73.65 59.937-133.588 133.587-133.588 34.199 0 67.311 13.25 92.015 36.648 22.719-5.197 44.484-13.93 64.808-25.983a13.737 13.737 0 0 1 15.399 1.061c4.408 3.537 6.204 9.414 4.489 14.801a132.455 132.455 0 0 1-18.99 37.519 214.647 214.647 0 0 0 16.678-6.584c5.469-2.503 12.025-.979 15.916 3.7 3.891 4.625 4.245 11.291.898 16.324-15.808 23.562-35.07 44.103-57.299 61.162.082 2.938.109 5.904.109 8.842.001 174.453-132.798 354.945-354.973 354.945zm-128.69-53.543c40.458 17.33 84.125 26.336 128.69 26.336 205.17 0 327.766-166.645 327.766-327.765 0-4.979-.136-9.985-.326-14.964a13.59 13.59 0 0 1 5.632-11.59 228.226 228.226 0 0 0 27.507-23.371 255.254 255.254 0 0 1-26.99 4.707c-6.312.898-12.406-2.993-14.501-9.087-2.068-6.067.381-12.76 5.877-16.08a107.521 107.521 0 0 0 28.051-24.296c-13.577 5.197-27.562 9.25-41.845 12.025-4.571.953-9.333-.571-12.543-4.026-20.024-21.331-48.348-33.574-77.677-33.574-58.659 0-106.38 47.721-106.38 106.38 0 8.135.925 16.324 2.774 24.269a13.662 13.662 0 0 1-2.829 11.808c-2.721 3.265-6.829 5.523-11.101 4.843-92.858-4.68-180.33-45.953-243.179-114.189a106.518 106.518 0 0 0-6.094 35.533c0 35.696 17.685 68.753 47.286 88.533a13.578 13.578 0 0 1 5.414 15.454c-1.85 5.768-7.591 9.25-13.358 9.468a133.043 133.043 0 0 1-38.172-6.856c8.407 42.144 40.811 75.555 83.146 84.07 6.203 1.252 10.719 6.557 10.91 12.869.218 6.285-3.917 11.916-9.985 13.604-11.726 3.184-24.051 4.979-36.104 4.734 18.229 32.24 52.456 53.217 90.655 53.979 5.741.109 10.801 3.863 12.624 9.305s0 11.482-4.517 15.02c-38.226 29.953-83.01 47.964-130.731 52.861z"/></svg>
|
After Width: | Height: | Size: 2.2 KiB |
1
static/images/logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="493" height="161" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><g fill="#FFF"><path d="M328.52 37.36c-27.017 0-40.97 19.323-40.97 43.16 0 23.837 13.61 43.162 40.97 43.162s40.968-19.325 40.968-43.163c0-23.836-13.954-43.16-40.969-43.16zm20.438 43.237c-.02 15.328-5.126 27.743-20.439 27.743-15.313 0-20.42-12.414-20.436-27.743v-.077c.016-15.327 5.124-27.741 20.437-27.741 15.312 0 20.419 12.414 20.438 27.741v.077zM207.553 5.19c0-1.103.885-2.124 1.984-2.282 0 0 13.577-1.95 14.784-2.115 1.366-.187 3.181.798 3.181 2.744v44.236c3.23-3.105 6.785-5.608 10.66-7.515 3.88-1.906 8.428-2.86 13.653-2.86 4.524 0 8.532.776 12.032 2.33 3.502 1.55 6.423 3.73 8.765 6.533 2.342 2.806 4.12 6.155 5.332 10.05 1.21 3.893 1.817 8.182 1.817 12.866v51.352a1.998 1.998 0 0 1-2.005 1.994h-15.942a2.006 2.006 0 0 1-2.004-1.994V69.177c0-5.118-1.171-9.081-3.513-11.888-2.344-2.803-5.857-4.206-10.543-4.206-3.446 0-6.675.79-9.69 2.37-3.016 1.58-5.87 3.73-8.562 6.455v58.617c0 1.104-.896 2-2.004 2h-15.941a2 2 0 0 1-2.004-1.997V5.189zM451.56 100.517V56.836h-13.482c-1.1 0-1.742-.87-1.443-1.915 0 0 2.741-9.59 3.001-10.495.261-.905.941-1.877 2.307-2.07l9.597-1.353 3.508-23.488c.163-1.092 1.18-2.104 2.274-2.26 0 0 9.192-1.31 10.963-1.578 1.673-.253 3.189.965 3.189 2.808v24.518h17.566c1.106 0 2.002.897 2.002 2.005v11.823a2 2 0 0 1-2.002 2.005h-17.566v43.078c0 6.019 3.623 8.319 7.095 8.319 2.123 0 5.03-1.14 7.198-2.158 1.34-.626 3.417-.162 3.954 1.73l2.45 8.645c.302 1.067-.247 2.364-1.226 2.86 0 0-7.283 4.364-17.053 4.364-13.728 0-22.332-8.081-22.332-23.157zM406.976 52.778c-7.084 0-12.657 2.475-12.657 8.432 0 7.44 12.011 9.606 20.234 12.64 5.497 2.027 20.238 5.98 20.238 22.016 0 19.479-15.994 27.807-33.055 27.807-17.062 0-25.4-5.465-25.4-5.465-.962-.527-1.5-1.822-1.2-2.889 0 0 2.104-7.52 2.64-9.387.485-1.68 2.415-2.27 3.645-1.792 4.391 1.712 12.32 4.092 21.283 4.092 9.075 0 13.465-2.803 13.465-8.777 0-7.951-12.254-10.381-20.358-12.967-5.583-1.78-20.36-5.93-20.36-23.566 0-17.373 15.082-25.524 31.202-25.524 13.645 0 23.507 4.691 23.507 4.691 1.01.426 1.585 1.634 1.284 2.697 0 0-2.24 7.894-2.653 9.357-.489 1.739-1.899 2.537-3.667 1.957-3.888-1.277-11.197-3.322-18.148-3.322zM196.663 37.495c-6.695.776-11.472 3.963-14.562 6.93-6.068-4.81-14.49-7.105-23.944-7.105-18.953 0-33.76 9.252-33.76 29.426 0 11.582 4.873 19.562 12.614 24.26-5.749 2.752-9.577 8.592-9.577 14.333 0 9.604 7.501 12.612 7.501 12.612s-13.116 6.439-13.116 19.32c0 16.492 15.004 23.16 33.338 23.16 26.428 0 44.61-11.041 44.61-31.313 0-12.477-9.439-19.364-30.009-20.183-12.207-.488-20.114-.932-22.073-1.588-2.588-.87-3.86-2.965-3.86-5.28 0-2.554 2.074-4.986 5.345-6.656 2.853.51 5.863.763 8.99.763 18.968 0 33.761-9.225 33.761-29.427 0-4.898-.874-9.15-2.464-12.784 2.787-1.504 8.334-2.246 8.334-2.246 1.091-.174 1.976-1.213 1.975-2.31l-.001-9.132c0-1.88-1.588-2.955-3.102-2.78zm-49.13 85.133s9.954.381 19.9.847c11.172.523 14.654 2.958 14.654 8.81 0 7.15-9.71 14.104-23.28 14.104-12.88 0-19.314-4.533-19.314-12.08 0-4.33 2.26-9.173 8.04-11.68zm10.66-40.536c-8.978 0-15.983-4.824-15.983-15.346 0-10.523 7.011-15.346 15.983-15.346 8.974 0 15.984 4.81 15.984 15.346 0 10.536-7.002 15.346-15.984 15.346z"/></g><g opacity=".7" transform="translate(0 36)" fill="#F6F8FA"><path d="M.21 73.017c0-2.209 1.784-4 3.99-4h25.66a3.994 3.994 0 0 1 3.992 4v9.015c0 2.21-1.785 4-3.991 4H4.2a3.994 3.994 0 0 1-3.992-4v-9.015zM50.672 73.017c0-2.209 1.784-4 4.006-4h25.61a4.001 4.001 0 0 1 4.006 4v9.015c0 2.21-1.784 4-4.006 4h-25.61a4.001 4.001 0 0 1-4.006-4v-9.015z"/><rect x=".184" y="34.99" width="84.121" height="17.014" rx="4"/><path d="M.21 4.963c0-2.208 1.795-4 4.001-4h42.465a4 4 0 0 1 4.002 4v9.015c0 2.209-1.796 4-4.002 4H4.211a4 4 0 0 1-4.002-4V4.963z"/><rect x="67.494" y=".964" width="16.821" height="17.013" rx="4"/></g></g></svg>
|
After Width: | Height: | Size: 3.7 KiB |
2
static/robots.txt
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|