This commit is contained in:
Hippo 2019-09-19 16:43:37 +05:30
commit 3cafb57c40
58 changed files with 19221 additions and 0 deletions

26
.editorconfig Normal file
View 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
View file

@ -0,0 +1,3 @@
public/**
plugins/**/*.js
!plugins/*/src/*.js

59
.eslintrc.js Normal file
View 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
View 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
View 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:

View 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
View 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
View 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
View 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.
&nbsp;
# 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.
&nbsp;
# Copyright & License
Copyright (c) 2013-2019 Ghost Foundation - Released under the [MIT license](LICENSE).

32
gatsby-browser.js Executable file
View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
[build]
command = "gatsby build"
publish = "public/"
[template]
incoming-hooks = ["Ghost"]

61
package.json Executable file
View 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"
}
}

View file

@ -0,0 +1,10 @@
{
"presets": [
[
"babel-preset-gatsby-package",
{
"browser": true
}
]
]
}

View 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;
}
}
};

View 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);
};
}();

View 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);
};

View file

@ -0,0 +1 @@
// noop

View 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__"
}
}

View 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
}
}
}

View 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()
}
}

View 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)
}

File diff suppressed because it is too large Load diff

View 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 &mdash; 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

View 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

View 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

View 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

View 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'

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1 @@
export { default as MetaData } from './MetaData'

BIN
src/images/ghost-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

18
src/pages/404.js Executable file
View 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

File diff suppressed because it is too large Load diff

96
src/templates/author.js Executable file
View 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
View 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
View 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
View 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
View 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
View 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
}
}
`

View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View 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

View 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

View 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

View 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
View 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
View file

@ -0,0 +1,2 @@
User-agent: *
Allow: /

12642
yarn.lock Normal file

File diff suppressed because it is too large Load diff