├── .babelrc ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── app.json ├── bin ├── cleanup.js ├── hello.js └── setup.js ├── contentful └── export.json ├── gatsby-config.js ├── gatsby-node.js ├── package.json ├── screenshot.jpg ├── setup.jpg ├── src ├── components │ ├── article-preview.js │ ├── article-preview.module.css │ ├── base.css │ ├── container.js │ ├── hero.js │ ├── hero.module.css │ ├── layout.js │ ├── navigation.js │ └── navigation.module.css ├── pages │ ├── blog.js │ ├── blog.module.css │ └── index.js └── templates │ ├── blog-post.js │ └── blog-post.module.css ├── static.json ├── static ├── avenir-400.woff2 ├── favicon.ico └── robots.txt └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "loose": true, 7 | "modules": false, 8 | "useBuiltIns": "usage", 9 | "corejs": "^2.1.*", 10 | "shippedProposals": true, 11 | "targets": { 12 | "browsers": [">0.25%", "not dead"], 13 | }, 14 | }, 15 | ], 16 | [ 17 | "@babel/preset-react", 18 | { 19 | "useBuiltIns": true, 20 | "pragma": "React.createElement", 21 | }, 22 | ], 23 | ], 24 | "plugins": [ 25 | [ 26 | "@babel/plugin-proposal-class-properties", 27 | { 28 | "loose": true, 29 | }, 30 | ], 31 | "@babel/plugin-syntax-dynamic-import", 32 | "babel-plugin-macros", 33 | [ 34 | "@babel/plugin-transform-runtime", 35 | { 36 | "helpers": true, 37 | "regenerator": true, 38 | }, 39 | ], 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CONTENTFUL_SPACE_ID='' 2 | CONTENTFUL_ACCESS_TOKEN='' 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | }, 6 | "plugins": [ 7 | "react", 8 | ], 9 | "globals": { 10 | "graphql": false, 11 | }, 12 | "parserOptions": { 13 | "sourceType": "module", 14 | "ecmaFeatures": { 15 | "experimentalObjectRestSpread": true, 16 | "jsx": true, 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # build folder 61 | public 62 | 63 | # gatsby cache folder 64 | .cache 65 | 66 | # secrets 67 | .contentful.json 68 | .env* 69 | !.env.example 70 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 13.13.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | os: 4 | - linux 5 | - osx 6 | 7 | node_js: 8 | - "node" 9 | - "lts/*" 10 | - "7" 11 | - "8" 12 | 13 | script: 14 | - npm install 15 | - npm run lint 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ⚠️ Please use [the officially supported Contentful Gatsby starter](https://github.com/contentful/starter-gatsby-blog). 2 | 3 | # gatsby-contentful-starter 4 | 5 | Gatsby [Contentful](https://www.contentful.com) starter for creating a blog 6 | 7 | ![The index page of the starter blog](https://rawgit.com/contentful-userland/gatsby-contentful-starter/master/screenshot.jpg "The index page of the starter blog") 8 | 9 | Static sites are scalable, secure and have very little required maintenance. They come with a drawback though. Not everybody feels good editing files, building a project and uploading it somewhere. This is where Contentful comes into play. 10 | 11 | With Contentful and Gatsby you can connect your favorite static site generator with an API that provides an easy to use interface for people writing content and automate the publishing using services like [Travis CI](https://travis-ci.org/) or [Netlify](https://www.netlify.com/). 12 | 13 | ## Features 14 | 15 | * Simple content model and structure. Easy to adjust to your needs. 16 | * Contentful integration using our [Sync API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/synchronization/initial-synchronization-of-entries-of-a-specific-content-type) 17 | * Using our [Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/). 18 | * Responsive/adaptive images via [gatsby-image](https://www.gatsbyjs.org/packages/gatsby-image/) 19 | 20 | ## Contribution 21 | 22 | This project is part of [contentful-userland](https://github.com/contentful-userland) which means that we’re always open to contributions **and you can be part of userland and shape the project yourself after your first merged pull request**. You can learn more about how contentful userland is organized by visiting [our about repository](https://github.com/contentful-userland/about). 23 | 24 | ## Requirements 25 | 26 | To use this project you have to have a Contentful account. If you don't have one yet you can register at [www.contentful.com/sign-up](https://www.contentful.com/sign-up/). 27 | 28 | ## Getting started 29 | 30 | Install [Yarn](https://yarnpkg.com/en/docs/install) (if you haven't already). 31 | 32 | ### Get the source code and install dependencies. 33 | 34 | ``` 35 | $ git clone git@github.com:contentful-userland/gatsby-contentful-starter.git 36 | $ yarn install 37 | ``` 38 | 39 | Or use the [Gatsby CLI](https://www.npmjs.com/package/gatsby-cli). 40 | 41 | ``` 42 | $ gatsby new contentful-starter https://github.com/contentful-userland/gatsby-contentful-starter 43 | ``` 44 | 45 | ### Set up of the needed content model and create a configuration file 46 | 47 | This project comes with a Contentful setup command `yarn run setup`. 48 | 49 | ![Command line dialog of the yarn run setup command](https://rawgit.com/contentful-userland/gatsby-contentful-starter/master/setup.jpg "Command line dialog of the yarn run setup command") 50 | 51 | This command will ask you for a space ID, and access tokens for the Contentful Management and Delivery API and then import the needed content model into the space you define and write a config file (`./contentful.json`). 52 | 53 | `yarn run setup` automates that for you but if you want to do it yourself rename `.contentful.json.sample` to `.contentful.json` and add your configuration in this file. 54 | 55 | ## Crucial Commands 56 | 57 | This project comes with a few handy commands for linting and code fixing. The most important ones are the ones to develop and ship code. You can find the most important commands below. 58 | 59 | ### `yarn run dev` 60 | 61 | Run in the project locally. 62 | 63 | ### `yarn run build` 64 | 65 | Run a production build into `./public`. The result is ready to be put on any static hosting you prefer. 66 | 67 | ### `yarn run deploy` 68 | 69 | Run a production build into `./public` and publish the site to GitHub pages. 70 | 71 | ### `yarn run cleanup-repository` 72 | 73 | Removes all dependencies, scripts and data from the installation script. 74 | 75 | ## Roadmap 76 | 77 | - [x] [make the starter completely responsive](https://github.com/contentful-userland/gatsby-contentful-starter/issues/2) 78 | - [ ] [include tags](https://github.com/contentful-userland/gatsby-contentful-starter/issues/3) 79 | - [x] [support traced placeholders](https://github.com/contentful-userland/gatsby-contentful-starter/issues/4) 80 | - [ ] [add i18n](https://github.com/contentful-userland/gatsby-contentful-starter/issues/6) 81 | 82 | ## Other resources 83 | 84 | - Tutorial video series ["Building a blazing fast website with GatsbyJS and Contentful"](https://www.youtube.com/watch?v=Ek4o40w1tH4&list=PL8KiuH6vpACV-F7jXribe4YveGBhBeG9A) by @Khaledgarbaya 85 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contentful Gatsby Starter", 3 | "description": "Gatsby starter for a Contentful project.", 4 | "keywords": [ 5 | "contentful", 6 | "gatsby", 7 | "static", 8 | "ssg" 9 | ], 10 | "repository": "https://github.com/contentful-userland/gatsby-contentful-starter", 11 | "success_url": "/", 12 | "buildpacks": [ 13 | { 14 | "url": "heroku/nodejs" 15 | }, 16 | { 17 | "url": "https://github.com/heroku/heroku-buildpack-static" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /bin/cleanup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | let packageJSON = require(path.resolve(__dirname, '../package.json')) 6 | 7 | delete packageJSON.scripts['setup'] 8 | delete packageJSON.scripts['postinstall'] 9 | delete packageJSON.scripts['cleanup-repository'] 10 | 11 | let data = JSON.stringify(packageJSON, null, 2) 12 | fs.writeFileSync(path.resolve(__dirname, '../package.json'), data) 13 | -------------------------------------------------------------------------------- /bin/hello.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk') 2 | const pkg = require('../package.json') 3 | 4 | console.log(` 5 | 6 | ${chalk.green('Hey there! 👋')} 7 | 8 | Thanks for giving the ${pkg.name} a try. 🎉 9 | To get you going really quickly this project includes a setup step. 10 | 11 | ${chalk.yellow.bold('yarn run setup')} automates the following steps for you: 12 | - creates a config file ${chalk.yellow('./.contentful.json')} 13 | - imports ${chalk.green('a predefined content model')} 14 | 15 | When this is done run: 16 | 17 | ${chalk.yellow( 18 | 'yarn run dev' 19 | )} to start a development environment at ${chalk.green('localhost:8000')} 20 | 21 | or 22 | 23 | ${chalk.yellow( 24 | 'yarn run build' 25 | )} to create a production ready static site in ${chalk.green('./public')} 26 | 27 | For further information check the readme of the project 28 | (https://github.com/contentful-userland/gatsby-contentful-starter) 29 | 30 | `) 31 | -------------------------------------------------------------------------------- /bin/setup.js: -------------------------------------------------------------------------------- 1 | const spaceImport = require('contentful-import') 2 | const exportFile = require('../contentful/export.json') 3 | const inquirer = require('inquirer') 4 | const chalk = require('chalk') 5 | const path = require('path') 6 | const { writeFileSync } = require('fs') 7 | 8 | const argv = require('yargs-parser')(process.argv.slice(2)) 9 | 10 | console.log(` 11 | To set up this project you need to provide your Space ID 12 | and the belonging API access tokens. 13 | 14 | You can find all the needed information in your Contentful space under: 15 | 16 | ${chalk.yellow( 17 | `app.contentful.com ${chalk.red('->')} Space Settings ${chalk.red( 18 | '->' 19 | )} API keys` 20 | )} 21 | 22 | The ${chalk.green('Content Management API Token')} 23 | will be used to import and write data to your space. 24 | 25 | The ${chalk.green('Content Delivery API Token')} 26 | will be used to ship published production-ready content in your Gatsby app. 27 | 28 | The ${chalk.green('Content Preview API Token')} 29 | will be used to show not published data in your development environment. 30 | 31 | Ready? Let's do it! 🎉 32 | `) 33 | 34 | const questions = [ 35 | { 36 | name: 'spaceId', 37 | message: 'Your Space ID', 38 | when: !argv.spaceId && !process.env.CONTENTFUL_SPACE_ID, 39 | validate: input => 40 | /^[a-z0-9]{12}$/.test(input) || 41 | 'Space ID must be 12 lowercase characters', 42 | }, 43 | { 44 | name: 'managementToken', 45 | when: !argv.managementToken, 46 | message: 'Your Content Management API access token', 47 | }, 48 | { 49 | name: 'accessToken', 50 | when: !argv.accessToken && !process.env.CONTENTFUL_ACCESS_TOKEN_TOKEN, 51 | message: 'Your Content Delivery API access token', 52 | }, 53 | ] 54 | 55 | inquirer 56 | .prompt(questions) 57 | .then(({ spaceId, managementToken, accessToken }) => { 58 | const { CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN } = process.env 59 | 60 | // env vars are given precedence followed by args provided to the setup 61 | // followed by input given to prompts displayed by the setup script 62 | spaceId = CONTENTFUL_SPACE_ID || argv.spaceId || spaceId 63 | managementToken = argv.managementToken || managementToken 64 | accessToken = CONTENTFUL_ACCESS_TOKEN || argv.accessToken || accessToken 65 | 66 | console.log('Writing config file...') 67 | const configFiles = [`.env.development`, `.env.production`].map(file => 68 | path.join(__dirname, '..', file) 69 | ) 70 | 71 | const fileContents = 72 | [ 73 | `# All environment variables will be sourced`, 74 | `# and made available to gatsby-config.js, gatsby-node.js, etc.`, 75 | `# Do NOT commit this file to source control`, 76 | `CONTENTFUL_SPACE_ID='${spaceId}'`, 77 | `CONTENTFUL_ACCESS_TOKEN='${accessToken}'`, 78 | ].join('\n') + '\n' 79 | 80 | configFiles.forEach(file => { 81 | writeFileSync(file, fileContents, 'utf8') 82 | console.log(`Config file ${chalk.yellow(file)} written`) 83 | }) 84 | return { spaceId, managementToken } 85 | }) 86 | .then(({ spaceId, managementToken }) => 87 | spaceImport({ spaceId, managementToken, content: exportFile }) 88 | ) 89 | .then((_, error) => { 90 | console.log( 91 | `All set! You can now run ${chalk.yellow( 92 | 'yarn run dev' 93 | )} to see it in action.` 94 | ) 95 | }) 96 | .catch(error => console.error(error)) 97 | -------------------------------------------------------------------------------- /contentful/export.json: -------------------------------------------------------------------------------- 1 | { 2 | "contentTypes": [ 3 | { 4 | "sys": { 5 | "space": { 6 | "sys": { 7 | "type": "Link", 8 | "linkType": "Space", 9 | "id": "28p9vvm1oxuw" 10 | } 11 | }, 12 | "id": "person", 13 | "type": "ContentType", 14 | "createdAt": "2017-05-11T12:04:23.540Z", 15 | "updatedAt": "2017-05-18T14:04:51.650Z", 16 | "createdBy": { 17 | "sys": { 18 | "type": "Link", 19 | "linkType": "User", 20 | "id": "066RqBikAjzKy0SWUEtFvH" 21 | } 22 | }, 23 | "updatedBy": { 24 | "sys": { 25 | "type": "Link", 26 | "linkType": "User", 27 | "id": "2AAFsI4st4sZPlF1LFT13q" 28 | } 29 | }, 30 | "publishedCounter": 7, 31 | "version": 14, 32 | "publishedBy": { 33 | "sys": { 34 | "type": "Link", 35 | "linkType": "User", 36 | "id": "2AAFsI4st4sZPlF1LFT13q" 37 | } 38 | }, 39 | "publishedVersion": 13, 40 | "firstPublishedAt": "2017-05-11T12:04:23.869Z", 41 | "publishedAt": "2017-05-18T14:04:51.628Z" 42 | }, 43 | "displayField": "name", 44 | "name": "Person", 45 | "description": "", 46 | "fields": [ 47 | { 48 | "id": "name", 49 | "name": "Name", 50 | "type": "Symbol", 51 | "localized": false, 52 | "required": true, 53 | "validations": [], 54 | "disabled": false, 55 | "omitted": false 56 | }, 57 | { 58 | "id": "title", 59 | "name": "Title", 60 | "type": "Symbol", 61 | "localized": false, 62 | "required": true, 63 | "validations": [], 64 | "disabled": false, 65 | "omitted": false 66 | }, 67 | { 68 | "id": "company", 69 | "name": "Company", 70 | "type": "Symbol", 71 | "localized": false, 72 | "required": true, 73 | "validations": [], 74 | "disabled": false, 75 | "omitted": false 76 | }, 77 | { 78 | "id": "shortBio", 79 | "name": "Short Bio", 80 | "type": "Text", 81 | "localized": false, 82 | "required": true, 83 | "validations": [], 84 | "disabled": false, 85 | "omitted": false 86 | }, 87 | { 88 | "id": "email", 89 | "name": "Email", 90 | "type": "Symbol", 91 | "localized": false, 92 | "required": false, 93 | "validations": [], 94 | "disabled": false, 95 | "omitted": false 96 | }, 97 | { 98 | "id": "phone", 99 | "name": "Phone", 100 | "type": "Symbol", 101 | "localized": false, 102 | "required": false, 103 | "validations": [], 104 | "disabled": false, 105 | "omitted": false 106 | }, 107 | { 108 | "id": "facebook", 109 | "name": "Facebook", 110 | "type": "Symbol", 111 | "localized": false, 112 | "required": false, 113 | "validations": [], 114 | "disabled": false, 115 | "omitted": false 116 | }, 117 | { 118 | "id": "twitter", 119 | "name": "Twitter", 120 | "type": "Symbol", 121 | "localized": false, 122 | "required": false, 123 | "validations": [], 124 | "disabled": false, 125 | "omitted": false 126 | }, 127 | { 128 | "id": "github", 129 | "name": "Github", 130 | "type": "Symbol", 131 | "localized": false, 132 | "required": false, 133 | "validations": [], 134 | "disabled": false, 135 | "omitted": false 136 | }, 137 | { 138 | "id": "image", 139 | "name": "Image", 140 | "type": "Link", 141 | "localized": false, 142 | "required": false, 143 | "validations": [], 144 | "disabled": false, 145 | "omitted": false, 146 | "linkType": "Asset" 147 | } 148 | ] 149 | }, 150 | { 151 | "sys": { 152 | "space": { 153 | "sys": { 154 | "type": "Link", 155 | "linkType": "Space", 156 | "id": "28p9vvm1oxuw" 157 | } 158 | }, 159 | "id": "blogPost", 160 | "type": "ContentType", 161 | "createdAt": "2017-05-11T12:17:42.862Z", 162 | "updatedAt": "2017-05-17T10:55:24.031Z", 163 | "createdBy": { 164 | "sys": { 165 | "type": "Link", 166 | "linkType": "User", 167 | "id": "066RqBikAjzKy0SWUEtFvH" 168 | } 169 | }, 170 | "updatedBy": { 171 | "sys": { 172 | "type": "Link", 173 | "linkType": "User", 174 | "id": "066RqBikAjzKy0SWUEtFvH" 175 | } 176 | }, 177 | "publishedCounter": 6, 178 | "version": 12, 179 | "publishedBy": { 180 | "sys": { 181 | "type": "Link", 182 | "linkType": "User", 183 | "id": "066RqBikAjzKy0SWUEtFvH" 184 | } 185 | }, 186 | "publishedVersion": 11, 187 | "firstPublishedAt": "2017-05-11T12:17:43.175Z", 188 | "publishedAt": "2017-05-17T10:55:24.004Z" 189 | }, 190 | "displayField": "title", 191 | "name": "Blog Post", 192 | "description": "", 193 | "fields": [ 194 | { 195 | "id": "title", 196 | "name": "Title", 197 | "type": "Symbol", 198 | "localized": false, 199 | "required": true, 200 | "validations": [], 201 | "disabled": false, 202 | "omitted": false 203 | }, 204 | { 205 | "id": "slug", 206 | "name": "Slug", 207 | "type": "Symbol", 208 | "localized": false, 209 | "required": true, 210 | "validations": [], 211 | "disabled": false, 212 | "omitted": false 213 | }, 214 | { 215 | "id": "heroImage", 216 | "name": "Hero Image", 217 | "type": "Link", 218 | "localized": false, 219 | "required": true, 220 | "validations": [], 221 | "disabled": false, 222 | "omitted": false, 223 | "linkType": "Asset" 224 | }, 225 | { 226 | "id": "description", 227 | "name": "Description", 228 | "type": "Text", 229 | "localized": false, 230 | "required": true, 231 | "validations": [], 232 | "disabled": false, 233 | "omitted": false 234 | }, 235 | { 236 | "id": "body", 237 | "name": "Body", 238 | "type": "Text", 239 | "localized": false, 240 | "required": true, 241 | "validations": [], 242 | "disabled": false, 243 | "omitted": false 244 | }, 245 | { 246 | "id": "author", 247 | "name": "Author", 248 | "type": "Link", 249 | "localized": false, 250 | "required": false, 251 | "validations": [ 252 | { 253 | "linkContentType": [ 254 | "person" 255 | ] 256 | } 257 | ], 258 | "disabled": false, 259 | "omitted": false, 260 | "linkType": "Entry" 261 | }, 262 | { 263 | "id": "publishDate", 264 | "name": "Publish Date", 265 | "type": "Date", 266 | "localized": false, 267 | "required": true, 268 | "validations": [], 269 | "disabled": false, 270 | "omitted": false 271 | }, 272 | { 273 | "id": "tags", 274 | "name": "Tags", 275 | "type": "Array", 276 | "localized": false, 277 | "required": false, 278 | "validations": [], 279 | "disabled": false, 280 | "omitted": false, 281 | "items": { 282 | "type": "Symbol", 283 | "validations": [ 284 | { 285 | "in": [ 286 | "general", 287 | "javascript", 288 | "static-sites" 289 | ] 290 | } 291 | ] 292 | } 293 | } 294 | ] 295 | } 296 | ], 297 | "editorInterfaces": [ 298 | { 299 | "controls": [ 300 | { 301 | "fieldId": "name", 302 | "widgetId": "singleLine" 303 | }, 304 | { 305 | "fieldId": "title", 306 | "widgetId": "singleLine" 307 | }, 308 | { 309 | "fieldId": "company", 310 | "widgetId": "singleLine" 311 | }, 312 | { 313 | "fieldId": "shortBio", 314 | "widgetId": "markdown" 315 | }, 316 | { 317 | "fieldId": "email", 318 | "widgetId": "singleLine" 319 | }, 320 | { 321 | "fieldId": "phone", 322 | "widgetId": "singleLine" 323 | }, 324 | { 325 | "fieldId": "facebook", 326 | "widgetId": "singleLine" 327 | }, 328 | { 329 | "fieldId": "twitter", 330 | "widgetId": "singleLine" 331 | }, 332 | { 333 | "fieldId": "github", 334 | "widgetId": "singleLine" 335 | }, 336 | { 337 | "fieldId": "image", 338 | "widgetId": "assetLinkEditor" 339 | } 340 | ], 341 | "sys": { 342 | "id": "default", 343 | "type": "EditorInterface", 344 | "version": 14, 345 | "createdAt": "2017-05-11T12:04:24.145Z", 346 | "createdBy": { 347 | "sys": { 348 | "type": "Link", 349 | "linkType": "User", 350 | "id": "066RqBikAjzKy0SWUEtFvH" 351 | } 352 | }, 353 | "space": { 354 | "sys": { 355 | "type": "Link", 356 | "linkType": "Space", 357 | "id": "28p9vvm1oxuw" 358 | } 359 | }, 360 | "contentType": { 361 | "sys": { 362 | "type": "Link", 363 | "linkType": "ContentType", 364 | "id": "person" 365 | } 366 | }, 367 | "updatedAt": "2017-05-18T14:04:52.378Z", 368 | "updatedBy": { 369 | "sys": { 370 | "type": "Link", 371 | "linkType": "User", 372 | "id": "2AAFsI4st4sZPlF1LFT13q" 373 | } 374 | } 375 | } 376 | }, 377 | { 378 | "controls": [ 379 | { 380 | "fieldId": "title", 381 | "widgetId": "singleLine" 382 | }, 383 | { 384 | "fieldId": "slug", 385 | "widgetId": "slugEditor" 386 | }, 387 | { 388 | "fieldId": "heroImage", 389 | "widgetId": "assetLinkEditor" 390 | }, 391 | { 392 | "fieldId": "description", 393 | "widgetId": "markdown" 394 | }, 395 | { 396 | "fieldId": "body", 397 | "widgetId": "markdown" 398 | }, 399 | { 400 | "fieldId": "author", 401 | "widgetId": "entryLinkEditor" 402 | }, 403 | { 404 | "fieldId": "publishDate", 405 | "widgetId": "datePicker", 406 | "settings": { 407 | "format": "timeZ", 408 | "ampm": "24" 409 | } 410 | }, 411 | { 412 | "fieldId": "tags", 413 | "widgetId": "tagEditor" 414 | } 415 | ], 416 | "sys": { 417 | "id": "default", 418 | "type": "EditorInterface", 419 | "version": 12, 420 | "createdAt": "2017-05-11T12:17:43.510Z", 421 | "createdBy": { 422 | "sys": { 423 | "type": "Link", 424 | "linkType": "User", 425 | "id": "066RqBikAjzKy0SWUEtFvH" 426 | } 427 | }, 428 | "space": { 429 | "sys": { 430 | "type": "Link", 431 | "linkType": "Space", 432 | "id": "28p9vvm1oxuw" 433 | } 434 | }, 435 | "contentType": { 436 | "sys": { 437 | "type": "Link", 438 | "linkType": "ContentType", 439 | "id": "blogPost" 440 | } 441 | }, 442 | "updatedAt": "2017-05-17T10:55:25.370Z", 443 | "updatedBy": { 444 | "sys": { 445 | "type": "Link", 446 | "linkType": "User", 447 | "id": "066RqBikAjzKy0SWUEtFvH" 448 | } 449 | } 450 | } 451 | } 452 | ], 453 | "entries": [ 454 | { 455 | "sys": { 456 | "space": { 457 | "sys": { 458 | "type": "Link", 459 | "linkType": "Space", 460 | "id": "28p9vvm1oxuw" 461 | } 462 | }, 463 | "id": "15jwOBqpxqSAOy2eOO4S0m", 464 | "type": "Entry", 465 | "createdAt": "2017-05-11T12:04:29.532Z", 466 | "updatedAt": "2017-05-18T14:13:14.463Z", 467 | "createdBy": { 468 | "sys": { 469 | "type": "Link", 470 | "linkType": "User", 471 | "id": "066RqBikAjzKy0SWUEtFvH" 472 | } 473 | }, 474 | "updatedBy": { 475 | "sys": { 476 | "type": "Link", 477 | "linkType": "User", 478 | "id": "2AAFsI4st4sZPlF1LFT13q" 479 | } 480 | }, 481 | "publishedCounter": 15, 482 | "version": 190, 483 | "publishedBy": { 484 | "sys": { 485 | "type": "Link", 486 | "linkType": "User", 487 | "id": "2AAFsI4st4sZPlF1LFT13q" 488 | } 489 | }, 490 | "publishedVersion": 189, 491 | "firstPublishedAt": "2017-05-11T12:06:33.065Z", 492 | "publishedAt": "2017-05-18T14:13:14.431Z", 493 | "contentType": { 494 | "sys": { 495 | "type": "Link", 496 | "linkType": "ContentType", 497 | "id": "person" 498 | } 499 | } 500 | }, 501 | "fields": { 502 | "name": { 503 | "en-US": "John Doe" 504 | }, 505 | "title": { 506 | "en-US": "Web Developer" 507 | }, 508 | "company": { 509 | "en-US": "ACME" 510 | }, 511 | "shortBio": { 512 | "en-US": "Research and recommendations for modern stack websites." 513 | }, 514 | "email": { 515 | "en-US": "john@doe.com" 516 | }, 517 | "phone": { 518 | "en-US": "0176 / 1234567" 519 | }, 520 | "facebook": { 521 | "en-US": "johndoe" 522 | }, 523 | "twitter": { 524 | "en-US": "johndoe" 525 | }, 526 | "github": { 527 | "en-US": "johndoe" 528 | }, 529 | "image": { 530 | "en-US": { 531 | "sys": { 532 | "type": "Link", 533 | "linkType": "Asset", 534 | "id": "7orLdboQQowIUs22KAW4U" 535 | } 536 | } 537 | } 538 | } 539 | }, 540 | { 541 | "sys": { 542 | "space": { 543 | "sys": { 544 | "type": "Link", 545 | "linkType": "Space", 546 | "id": "28p9vvm1oxuw" 547 | } 548 | }, 549 | "id": "31TNnjHlfaGUoMOwU0M2og", 550 | "type": "Entry", 551 | "createdAt": "2017-05-12T09:29:02.336Z", 552 | "updatedAt": "2017-05-30T12:55:11.986Z", 553 | "createdBy": { 554 | "sys": { 555 | "type": "Link", 556 | "linkType": "User", 557 | "id": "066RqBikAjzKy0SWUEtFvH" 558 | } 559 | }, 560 | "updatedBy": { 561 | "sys": { 562 | "type": "Link", 563 | "linkType": "User", 564 | "id": "2AAFsI4st4sZPlF1LFT13q" 565 | } 566 | }, 567 | "publishedCounter": 13, 568 | "version": 319, 569 | "publishedBy": { 570 | "sys": { 571 | "type": "Link", 572 | "linkType": "User", 573 | "id": "2AAFsI4st4sZPlF1LFT13q" 574 | } 575 | }, 576 | "publishedVersion": 318, 577 | "firstPublishedAt": "2017-05-12T09:30:09.613Z", 578 | "publishedAt": "2017-05-30T12:55:11.950Z", 579 | "contentType": { 580 | "sys": { 581 | "type": "Link", 582 | "linkType": "ContentType", 583 | "id": "blogPost" 584 | } 585 | } 586 | }, 587 | "fields": { 588 | "title": { 589 | "en-US": "Automate with webhooks" 590 | }, 591 | "slug": { 592 | "en-US": "automate-with-webhooks" 593 | }, 594 | "heroImage": { 595 | "en-US": { 596 | "sys": { 597 | "type": "Link", 598 | "linkType": "Asset", 599 | "id": "4shwYI3POEGkw0Eg6kcyaQ" 600 | } 601 | } 602 | }, 603 | "description": { 604 | "en-US": "Webhooks notify you, another person or system when resources have changed by calling a given HTTP endpoint." 605 | }, 606 | "body": { 607 | "en-US": "## What are webhooks?\n\nThe webhooks are used to notify you when content has been changed. Specify a URL, configure your webhook, and we will send an HTTP POST request whenever something happens to your content.\n\n## How do I configure a webhook?\n\nGo to Settings → Webhooks from the navigation bar at the top. From there, hit Add webhook, and you will be directed to your new webhook. Then choose a name, put in the information of your HTTP endpoint (URL and authentication), specify any custom headers and select the types of events that should trigger the webhook.\n\n## Why do I get an old version in the CDA?\n\nAs the delivery API is powered by a CDN network consisting of hundreds of servers distributed across continents, it takes some time (up to a few minutes) to reflect the changes to the published content. This must be taken into consideration when reacting to webhooks. In normal conditions, there could be a reasonable delay of 2 to 5 minutes.\n\nExtracted from the [Webhooks FAQ](https://www.contentful.com/faq/webhooks/ \"Webhooks FAQ\")." 608 | }, 609 | "author": { 610 | "en-US": { 611 | "sys": { 612 | "type": "Link", 613 | "linkType": "Entry", 614 | "id": "15jwOBqpxqSAOy2eOO4S0m" 615 | } 616 | } 617 | }, 618 | "publishDate": { 619 | "en-US": "2017-05-12T00:00+02:00" 620 | }, 621 | "tags": { 622 | "en-US": [ 623 | "javascript" 624 | ] 625 | } 626 | } 627 | }, 628 | { 629 | "sys": { 630 | "space": { 631 | "sys": { 632 | "type": "Link", 633 | "linkType": "Space", 634 | "id": "28p9vvm1oxuw" 635 | } 636 | }, 637 | "id": "3K9b0esdy0q0yGqgW2g6Ke", 638 | "type": "Entry", 639 | "createdAt": "2017-05-15T11:51:15.331Z", 640 | "updatedAt": "2017-05-30T09:48:09.568Z", 641 | "createdBy": { 642 | "sys": { 643 | "type": "Link", 644 | "linkType": "User", 645 | "id": "066RqBikAjzKy0SWUEtFvH" 646 | } 647 | }, 648 | "updatedBy": { 649 | "sys": { 650 | "type": "Link", 651 | "linkType": "User", 652 | "id": "2AAFsI4st4sZPlF1LFT13q" 653 | } 654 | }, 655 | "publishedCounter": 8, 656 | "version": 722, 657 | "publishedBy": { 658 | "sys": { 659 | "type": "Link", 660 | "linkType": "User", 661 | "id": "2AAFsI4st4sZPlF1LFT13q" 662 | } 663 | }, 664 | "publishedVersion": 721, 665 | "firstPublishedAt": "2017-05-15T11:51:40.463Z", 666 | "publishedAt": "2017-05-30T09:48:09.520Z", 667 | "contentType": { 668 | "sys": { 669 | "type": "Link", 670 | "linkType": "ContentType", 671 | "id": "blogPost" 672 | } 673 | } 674 | }, 675 | "fields": { 676 | "title": { 677 | "en-US": "Hello world" 678 | }, 679 | "slug": { 680 | "en-US": "hello-world" 681 | }, 682 | "heroImage": { 683 | "en-US": { 684 | "sys": { 685 | "type": "Link", 686 | "linkType": "Asset", 687 | "id": "6Od9v3wzLOysiMum0Wkmme" 688 | } 689 | } 690 | }, 691 | "description": { 692 | "en-US": "Your very first content with Contentful, pulled in JSON format using the Content Delivery API." 693 | }, 694 | "body": { 695 | "en-US": "These is your very first content with Contentful, pulled in JSON format using the [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/ \"Content Delivery API\"). Content and presentation are now decoupled, allowing you to focus your efforts in building the perfect app.\n\n## Your first steps\n\nBuilding with Contentful is easy. First take a moment to get [the basics of content modelling](https://www.contentful.com/r/knowledgebase/content-modelling-basics/ \"the basics of content modelling\"), which you can set up in the [Contentful Web app](https://app.contentful.com/ \"Contentful Web app\"). Once you get that, feel free to drop by the [Documentation](https://www.contentful.com/developers/docs/ \"Documentation\") to learn a bit more about how to build your app with Contentful, in particular the [API basics](https://www.contentful.com/developers/docs/concepts/apis/ \"API basics\") and each one of our four APIs, as shown below.\n\n### Content Delivery API\n\nThe [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/ \"Content Delivery API\") (CDA), available at `cdn.contentful.com`, is a read-only API for delivering content from Contentful to apps, websites and other media. Content is delivered as JSON data, and images, videos and other media as files.\nThe API is available via a globally distributed content delivery network. The server closest to the user serves all content, both JSON and binary. This minimizes latency, which especially benefits mobile apps. Hosting content in multiple global data centers also greatly improves the availability of content.\n\n### Content Management API\n\nThe [Content Management API](https://www.contentful.com/developers/docs/references/content-management-api/ \"Content Management API\") (CMA), available at `api.contentful.com`, is a read-write API for managing content. Unlike the Content Delivery API, the management API requires you to authenticate as a Contentful user. You could use the CMA for several use cases, such as:\n* Automatic imports from different CMSes like WordPress or Drupal.\n* Integration with other backend systems, such as an e-commerce shop.\n* Building custom editing experiences. We built the [Contentful Web app](https://app.contentful.com/ \"Contentful Web app\") on top of this API.\n\n### Preview API\n\nThe [Content Preview API](https://www.contentful.com/developers/docs/concepts/apis/#content-preview-api \"Content Preview API\"), available at `preview.contentful.com`, is a variant of the CDA for previewing your content before delivering it to your customers. You use the Content Preview API in combination with a \"preview\" deployment of your website (or a \"preview\" build of your mobile app) that allows content managers and authors to view their work in-context, as if it were published, using a \"preview\" access token as though it were delivered by the CDA.\n\n### Images API\n\nThe [Images API](https://www.contentful.com/developers/docs/concepts/apis/#images-api \"Images API\"), available at `images.contentful.com`, allows you to resize and crop images, change their background color and convert them to different formats. Using our API for these transformations lets you upload high-quality assets, deliver exactly what your app needs, and still get all the benefits of our caching CDN." 696 | }, 697 | "author": { 698 | "en-US": { 699 | "sys": { 700 | "type": "Link", 701 | "linkType": "Entry", 702 | "id": "15jwOBqpxqSAOy2eOO4S0m" 703 | } 704 | } 705 | }, 706 | "publishDate": { 707 | "en-US": "2017-05-15T00:00+02:00" 708 | }, 709 | "tags": { 710 | "en-US": [ 711 | "general" 712 | ] 713 | } 714 | } 715 | }, 716 | { 717 | "sys": { 718 | "space": { 719 | "sys": { 720 | "type": "Link", 721 | "linkType": "Space", 722 | "id": "28p9vvm1oxuw" 723 | } 724 | }, 725 | "id": "2PtC9h1YqIA6kaUaIsWEQ0", 726 | "type": "Entry", 727 | "createdAt": "2017-05-15T12:01:00.579Z", 728 | "updatedAt": "2017-05-30T12:31:34.521Z", 729 | "createdBy": { 730 | "sys": { 731 | "type": "Link", 732 | "linkType": "User", 733 | "id": "066RqBikAjzKy0SWUEtFvH" 734 | } 735 | }, 736 | "updatedBy": { 737 | "sys": { 738 | "type": "Link", 739 | "linkType": "User", 740 | "id": "2AAFsI4st4sZPlF1LFT13q" 741 | } 742 | }, 743 | "publishedCounter": 7, 744 | "version": 218, 745 | "publishedBy": { 746 | "sys": { 747 | "type": "Link", 748 | "linkType": "User", 749 | "id": "2AAFsI4st4sZPlF1LFT13q" 750 | } 751 | }, 752 | "publishedVersion": 217, 753 | "firstPublishedAt": "2017-05-15T12:01:52.543Z", 754 | "publishedAt": "2017-05-30T12:31:34.481Z", 755 | "contentType": { 756 | "sys": { 757 | "type": "Link", 758 | "linkType": "ContentType", 759 | "id": "blogPost" 760 | } 761 | } 762 | }, 763 | "fields": { 764 | "title": { 765 | "en-US": "Static sites are great" 766 | }, 767 | "slug": { 768 | "en-US": "static-sites-are-great" 769 | }, 770 | "heroImage": { 771 | "en-US": { 772 | "sys": { 773 | "type": "Link", 774 | "linkType": "Asset", 775 | "id": "4NzwDSDlGECGIiokKomsyI" 776 | } 777 | } 778 | }, 779 | "description": { 780 | "en-US": "Worry less about security, caching, and talking to the server. Static sites are the new thing." 781 | }, 782 | "body": { 783 | "en-US": "## The case for the static site generator\n\nMore and more developers are jumping on the \"go static train\", and rightfully so. Static pages are fast, lightweight, they scale well. They are more secure, and simple to maintain and they allow you to focus all your time and effort on the user interface. Often times, this dedication really shows.\n\nIt just so happens that static site generators are mostly loved by developers, but not by the average Joe. They do not offer WYSIWYG, previewing on demo sites may take an update cycle, they are often based on markdown text files, and they require some knowledge of modern day repositories.\n\nMoreover, when teams are collaborating, it can get complicated quickly. Has this article already been proof-read or reviewed? Is this input valid? Are user permissions available, e.g. for administering adding and removing team members? Can this article be published at a future date? How can a large repository of content be categorized, organized, and searched? All these requirements have previously been more or less solved within the admin area of your CMS. But of course with all the baggage that made you leave the appserver-app-database-in-one-big-blob stack in the first place.\n\n## Content APIs to the rescue\n\nAn alternative is decoupling the content management aspect from the system. And then replacing the maintenance prone server with a cloud based web service offering. Effectively, instead of your CMS of old, you move to a [Content Management as a Service (CMaaS)](https://www.contentful.com/r/knowledgebase/content-as-a-service/ \"Content Management as a Service (CMaaS)\") world, with a content API to deliver all your content. That way, you get the all the [benefits of content management features](http://www.digett.com/blog/01/16/2014/pairing-static-websites-cms \"benefits of content management features\") while still being able to embrace the static site generator mantra.\n\nIt so happens that Contentful is offering just that kind of content API. A service that\n\n* from the ground up has been designed to be fast, scalable, secure, and offer high uptime, so that you don’t have to worry about maintenance ever again.\n* offers a powerful editor and lots of flexibility in creating templates for your documents that your editors can reuse and combine, so that no developers resources are required in everyday writing and updating tasks.\n* separates content from presentation, so you can reuse your content repository for any device platform your heart desires. That way, you can COPE (\"create once, publish everywhere\").\n* offers webhooks that you can use to rebuild your static site in a fully automated fashion every time your content is modified.\n\nExtracted from the article [CMS-functionality for static site generators](https://www.contentful.com/r/knowledgebase/contentful-api-cms-static-site-generators/ \"CMS-functionality for static site generators\"). Read more about the [static site generators supported by Contentful](https://www.contentful.com/developers/docs/tools/staticsitegenerators/ \"static site generators supported by Contentful\")." 784 | }, 785 | "author": { 786 | "en-US": { 787 | "sys": { 788 | "type": "Link", 789 | "linkType": "Entry", 790 | "id": "15jwOBqpxqSAOy2eOO4S0m" 791 | } 792 | } 793 | }, 794 | "publishDate": { 795 | "en-US": "2017-05-16T00:00+02:00" 796 | }, 797 | "tags": { 798 | "en-US": [ 799 | "javascript", 800 | "static-sites" 801 | ] 802 | } 803 | } 804 | } 805 | ], 806 | "assets": [ 807 | { 808 | "sys": { 809 | "space": { 810 | "sys": { 811 | "type": "Link", 812 | "linkType": "Space", 813 | "id": "28p9vvm1oxuw" 814 | } 815 | }, 816 | "id": "7orLdboQQowIUs22KAW4U", 817 | "type": "Asset", 818 | "createdAt": "2017-05-11T13:04:42.667Z", 819 | "updatedAt": "2017-05-16T09:29:04.154Z", 820 | "createdBy": { 821 | "sys": { 822 | "type": "Link", 823 | "linkType": "User", 824 | "id": "066RqBikAjzKy0SWUEtFvH" 825 | } 826 | }, 827 | "updatedBy": { 828 | "sys": { 829 | "type": "Link", 830 | "linkType": "User", 831 | "id": "066RqBikAjzKy0SWUEtFvH" 832 | } 833 | }, 834 | "publishedCounter": 3, 835 | "version": 56, 836 | "publishedBy": { 837 | "sys": { 838 | "type": "Link", 839 | "linkType": "User", 840 | "id": "066RqBikAjzKy0SWUEtFvH" 841 | } 842 | }, 843 | "publishedVersion": 55, 844 | "firstPublishedAt": "2017-05-11T13:05:17.053Z", 845 | "publishedAt": "2017-05-16T09:29:04.140Z" 846 | }, 847 | "fields": { 848 | "title": { 849 | "en-US": "Sparkler" 850 | }, 851 | "description": { 852 | "en-US": "John with Sparkler" 853 | }, 854 | "file": { 855 | "en-US": { 856 | "url": "//images.contentful.com/28p9vvm1oxuw/7orLdboQQowIUs22KAW4U/a97cd3b3415b51c5facfa6f4d184b650/matt-palmer-254999.jpg", 857 | "details": { 858 | "size": 2293094, 859 | "image": { 860 | "width": 3000, 861 | "height": 2000 862 | } 863 | }, 864 | "fileName": "matt-palmer-254999.jpg", 865 | "contentType": "image/jpeg" 866 | } 867 | } 868 | } 869 | }, 870 | { 871 | "sys": { 872 | "space": { 873 | "sys": { 874 | "type": "Link", 875 | "linkType": "Space", 876 | "id": "28p9vvm1oxuw" 877 | } 878 | }, 879 | "id": "6Od9v3wzLOysiMum0Wkmme", 880 | "type": "Asset", 881 | "createdAt": "2017-05-15T12:05:53.234Z", 882 | "updatedAt": "2017-05-15T13:36:58.905Z", 883 | "createdBy": { 884 | "sys": { 885 | "type": "Link", 886 | "linkType": "User", 887 | "id": "066RqBikAjzKy0SWUEtFvH" 888 | } 889 | }, 890 | "updatedBy": { 891 | "sys": { 892 | "type": "Link", 893 | "linkType": "User", 894 | "id": "066RqBikAjzKy0SWUEtFvH" 895 | } 896 | }, 897 | "publishedCounter": 2, 898 | "version": 22, 899 | "publishedBy": { 900 | "sys": { 901 | "type": "Link", 902 | "linkType": "User", 903 | "id": "066RqBikAjzKy0SWUEtFvH" 904 | } 905 | }, 906 | "publishedVersion": 21, 907 | "firstPublishedAt": "2017-05-15T12:06:38.633Z", 908 | "publishedAt": "2017-05-15T13:36:58.888Z" 909 | }, 910 | "fields": { 911 | "title": { 912 | "en-US": "Woman with black hat" 913 | }, 914 | "description": { 915 | "en-US": "Woman wearing a black hat" 916 | }, 917 | "file": { 918 | "en-US": { 919 | "url": "//images.contentful.com/28p9vvm1oxuw/6Od9v3wzLOysiMum0Wkmme/95675d379a1284015a8210ca66cc53a5/cameron-kirby-88711.jpg", 920 | "details": { 921 | "size": 7316629, 922 | "image": { 923 | "width": 3000, 924 | "height": 2000 925 | } 926 | }, 927 | "fileName": "cameron-kirby-88711.jpg", 928 | "contentType": "image/jpeg" 929 | } 930 | } 931 | } 932 | }, 933 | { 934 | "sys": { 935 | "space": { 936 | "sys": { 937 | "type": "Link", 938 | "linkType": "Space", 939 | "id": "28p9vvm1oxuw" 940 | } 941 | }, 942 | "id": "4NzwDSDlGECGIiokKomsyI", 943 | "type": "Asset", 944 | "createdAt": "2017-05-15T12:33:11.841Z", 945 | "updatedAt": "2017-05-15T12:33:50.816Z", 946 | "createdBy": { 947 | "sys": { 948 | "type": "Link", 949 | "linkType": "User", 950 | "id": "066RqBikAjzKy0SWUEtFvH" 951 | } 952 | }, 953 | "updatedBy": { 954 | "sys": { 955 | "type": "Link", 956 | "linkType": "User", 957 | "id": "066RqBikAjzKy0SWUEtFvH" 958 | } 959 | }, 960 | "publishedCounter": 1, 961 | "version": 21, 962 | "publishedBy": { 963 | "sys": { 964 | "type": "Link", 965 | "linkType": "User", 966 | "id": "066RqBikAjzKy0SWUEtFvH" 967 | } 968 | }, 969 | "publishedVersion": 20, 970 | "firstPublishedAt": "2017-05-15T12:33:50.801Z", 971 | "publishedAt": "2017-05-15T12:33:50.801Z" 972 | }, 973 | "fields": { 974 | "title": { 975 | "en-US": "City" 976 | }, 977 | "description": { 978 | "en-US": "City pictured from the sky" 979 | }, 980 | "file": { 981 | "en-US": { 982 | "url": "//images.contentful.com/28p9vvm1oxuw/4NzwDSDlGECGIiokKomsyI/d04a5154fa2e2ab02857950639325684/denys-nevozhai-100695.jpg", 983 | "details": { 984 | "size": 15736986, 985 | "image": { 986 | "width": 3992, 987 | "height": 2992 988 | } 989 | }, 990 | "fileName": "denys-nevozhai-100695.jpg", 991 | "contentType": "image/jpeg" 992 | } 993 | } 994 | } 995 | }, 996 | { 997 | "sys": { 998 | "space": { 999 | "sys": { 1000 | "type": "Link", 1001 | "linkType": "Space", 1002 | "id": "28p9vvm1oxuw" 1003 | } 1004 | }, 1005 | "id": "4shwYI3POEGkw0Eg6kcyaQ", 1006 | "type": "Asset", 1007 | "createdAt": "2017-05-15T12:34:08.279Z", 1008 | "updatedAt": "2017-05-15T12:35:08.308Z", 1009 | "createdBy": { 1010 | "sys": { 1011 | "type": "Link", 1012 | "linkType": "User", 1013 | "id": "066RqBikAjzKy0SWUEtFvH" 1014 | } 1015 | }, 1016 | "updatedBy": { 1017 | "sys": { 1018 | "type": "Link", 1019 | "linkType": "User", 1020 | "id": "066RqBikAjzKy0SWUEtFvH" 1021 | } 1022 | }, 1023 | "publishedCounter": 1, 1024 | "version": 38, 1025 | "publishedBy": { 1026 | "sys": { 1027 | "type": "Link", 1028 | "linkType": "User", 1029 | "id": "066RqBikAjzKy0SWUEtFvH" 1030 | } 1031 | }, 1032 | "publishedVersion": 37, 1033 | "firstPublishedAt": "2017-05-15T12:35:08.292Z", 1034 | "publishedAt": "2017-05-15T12:35:08.292Z" 1035 | }, 1036 | "fields": { 1037 | "title": { 1038 | "en-US": "Man in the fields" 1039 | }, 1040 | "description": { 1041 | "en-US": "Tattooed man walking in a field" 1042 | }, 1043 | "file": { 1044 | "en-US": { 1045 | "url": "//images.contentful.com/28p9vvm1oxuw/4shwYI3POEGkw0Eg6kcyaQ/eeaa6df85fb4452ea69ad18c98ffc015/felix-russell-saw-112140.jpg", 1046 | "details": { 1047 | "size": 4539181, 1048 | "image": { 1049 | "width": 2500, 1050 | "height": 1667 1051 | } 1052 | }, 1053 | "fileName": "felix-russell-saw-112140.jpg", 1054 | "contentType": "image/jpeg" 1055 | } 1056 | } 1057 | } 1058 | } 1059 | ], 1060 | "locales": [ 1061 | { 1062 | "name": "U.S. English", 1063 | "code": "en-US", 1064 | "fallbackCode": null, 1065 | "default": true, 1066 | "contentManagementApi": true, 1067 | "contentDeliveryApi": true, 1068 | "optional": false, 1069 | "sys": { 1070 | "type": "Locale", 1071 | "id": "3Bb4qrZtqLzwN35dmTQ6a2", 1072 | "version": 0, 1073 | "space": { 1074 | "sys": { 1075 | "type": "Link", 1076 | "linkType": "Space", 1077 | "id": "28p9vvm1oxuw" 1078 | } 1079 | }, 1080 | "createdBy": { 1081 | "sys": { 1082 | "type": "Link", 1083 | "linkType": "User", 1084 | "id": "066RqBikAjzKy0SWUEtFvH" 1085 | } 1086 | }, 1087 | "createdAt": "2017-05-11T12:01:17Z", 1088 | "updatedBy": { 1089 | "sys": { 1090 | "type": "Link", 1091 | "linkType": "User", 1092 | "id": "066RqBikAjzKy0SWUEtFvH" 1093 | } 1094 | }, 1095 | "updatedAt": "2017-05-11T12:01:17Z" 1096 | } 1097 | } 1098 | ], 1099 | "webhooks": [], 1100 | "roles": [ 1101 | { 1102 | "name": "Author", 1103 | "description": "Allows editing of content", 1104 | "policies": [ 1105 | { 1106 | "effect": "allow", 1107 | "actions": [ 1108 | "create" 1109 | ], 1110 | "constraint": { 1111 | "and": [ 1112 | { 1113 | "equals": [ 1114 | { 1115 | "doc": "sys.type" 1116 | }, 1117 | "Entry" 1118 | ] 1119 | } 1120 | ] 1121 | } 1122 | }, 1123 | { 1124 | "effect": "allow", 1125 | "actions": [ 1126 | "read" 1127 | ], 1128 | "constraint": { 1129 | "and": [ 1130 | { 1131 | "equals": [ 1132 | { 1133 | "doc": "sys.type" 1134 | }, 1135 | "Entry" 1136 | ] 1137 | } 1138 | ] 1139 | } 1140 | }, 1141 | { 1142 | "effect": "allow", 1143 | "actions": [ 1144 | "update" 1145 | ], 1146 | "constraint": { 1147 | "and": [ 1148 | { 1149 | "equals": [ 1150 | { 1151 | "doc": "sys.type" 1152 | }, 1153 | "Entry" 1154 | ] 1155 | } 1156 | ] 1157 | } 1158 | }, 1159 | { 1160 | "effect": "allow", 1161 | "actions": [ 1162 | "create" 1163 | ], 1164 | "constraint": { 1165 | "and": [ 1166 | { 1167 | "equals": [ 1168 | { 1169 | "doc": "sys.type" 1170 | }, 1171 | "Asset" 1172 | ] 1173 | } 1174 | ] 1175 | } 1176 | }, 1177 | { 1178 | "effect": "allow", 1179 | "actions": [ 1180 | "read" 1181 | ], 1182 | "constraint": { 1183 | "and": [ 1184 | { 1185 | "equals": [ 1186 | { 1187 | "doc": "sys.type" 1188 | }, 1189 | "Asset" 1190 | ] 1191 | } 1192 | ] 1193 | } 1194 | }, 1195 | { 1196 | "effect": "allow", 1197 | "actions": [ 1198 | "update" 1199 | ], 1200 | "constraint": { 1201 | "and": [ 1202 | { 1203 | "equals": [ 1204 | { 1205 | "doc": "sys.type" 1206 | }, 1207 | "Asset" 1208 | ] 1209 | } 1210 | ] 1211 | } 1212 | } 1213 | ], 1214 | "permissions": { 1215 | "ContentModel": [ 1216 | "read" 1217 | ], 1218 | "Settings": [], 1219 | "ContentDelivery": [] 1220 | }, 1221 | "sys": { 1222 | "type": "Role", 1223 | "id": "3Bc1MhEh97SAPAubIwDlgq", 1224 | "version": 0, 1225 | "space": { 1226 | "sys": { 1227 | "type": "Link", 1228 | "linkType": "Space", 1229 | "id": "28p9vvm1oxuw" 1230 | } 1231 | }, 1232 | "createdBy": { 1233 | "sys": { 1234 | "type": "Link", 1235 | "linkType": "User", 1236 | "id": "066RqBikAjzKy0SWUEtFvH" 1237 | } 1238 | }, 1239 | "createdAt": "2017-05-11T12:01:17Z", 1240 | "updatedBy": { 1241 | "sys": { 1242 | "type": "Link", 1243 | "linkType": "User", 1244 | "id": "066RqBikAjzKy0SWUEtFvH" 1245 | } 1246 | }, 1247 | "updatedAt": "2017-05-11T12:01:17Z" 1248 | } 1249 | }, 1250 | { 1251 | "name": "Developer", 1252 | "description": "Allows reading Entries and managing API Keys", 1253 | "policies": [ 1254 | { 1255 | "effect": "allow", 1256 | "actions": [ 1257 | "read" 1258 | ], 1259 | "constraint": { 1260 | "and": [ 1261 | { 1262 | "equals": [ 1263 | { 1264 | "doc": "sys.type" 1265 | }, 1266 | "Entry" 1267 | ] 1268 | } 1269 | ] 1270 | } 1271 | }, 1272 | { 1273 | "effect": "allow", 1274 | "actions": [ 1275 | "read" 1276 | ], 1277 | "constraint": { 1278 | "and": [ 1279 | { 1280 | "equals": [ 1281 | { 1282 | "doc": "sys.type" 1283 | }, 1284 | "Asset" 1285 | ] 1286 | } 1287 | ] 1288 | } 1289 | } 1290 | ], 1291 | "permissions": { 1292 | "ContentModel": [ 1293 | "read" 1294 | ], 1295 | "Settings": [], 1296 | "ContentDelivery": "all" 1297 | }, 1298 | "sys": { 1299 | "type": "Role", 1300 | "id": "3Bdf87ZgV69DZme5Pll2MC", 1301 | "version": 0, 1302 | "space": { 1303 | "sys": { 1304 | "type": "Link", 1305 | "linkType": "Space", 1306 | "id": "28p9vvm1oxuw" 1307 | } 1308 | }, 1309 | "createdBy": { 1310 | "sys": { 1311 | "type": "Link", 1312 | "linkType": "User", 1313 | "id": "066RqBikAjzKy0SWUEtFvH" 1314 | } 1315 | }, 1316 | "createdAt": "2017-05-11T12:01:17Z", 1317 | "updatedBy": { 1318 | "sys": { 1319 | "type": "Link", 1320 | "linkType": "User", 1321 | "id": "066RqBikAjzKy0SWUEtFvH" 1322 | } 1323 | }, 1324 | "updatedAt": "2017-05-11T12:01:17Z" 1325 | } 1326 | }, 1327 | { 1328 | "name": "Editor", 1329 | "description": "Allows editing, publishing and archiving of content", 1330 | "policies": [ 1331 | { 1332 | "effect": "allow", 1333 | "actions": "all", 1334 | "constraint": { 1335 | "and": [ 1336 | { 1337 | "equals": [ 1338 | { 1339 | "doc": "sys.type" 1340 | }, 1341 | "Entry" 1342 | ] 1343 | } 1344 | ] 1345 | } 1346 | }, 1347 | { 1348 | "effect": "allow", 1349 | "actions": "all", 1350 | "constraint": { 1351 | "and": [ 1352 | { 1353 | "equals": [ 1354 | { 1355 | "doc": "sys.type" 1356 | }, 1357 | "Asset" 1358 | ] 1359 | } 1360 | ] 1361 | } 1362 | } 1363 | ], 1364 | "permissions": { 1365 | "ContentModel": [ 1366 | "read" 1367 | ], 1368 | "Settings": [], 1369 | "ContentDelivery": [] 1370 | }, 1371 | "sys": { 1372 | "type": "Role", 1373 | "id": "3BehG3o7XDtAd7i5NLYesC", 1374 | "version": 0, 1375 | "space": { 1376 | "sys": { 1377 | "type": "Link", 1378 | "linkType": "Space", 1379 | "id": "28p9vvm1oxuw" 1380 | } 1381 | }, 1382 | "createdBy": { 1383 | "sys": { 1384 | "type": "Link", 1385 | "linkType": "User", 1386 | "id": "066RqBikAjzKy0SWUEtFvH" 1387 | } 1388 | }, 1389 | "createdAt": "2017-05-11T12:01:17Z", 1390 | "updatedBy": { 1391 | "sys": { 1392 | "type": "Link", 1393 | "linkType": "User", 1394 | "id": "066RqBikAjzKy0SWUEtFvH" 1395 | } 1396 | }, 1397 | "updatedAt": "2017-05-11T12:01:17Z" 1398 | } 1399 | }, 1400 | { 1401 | "name": "Freelancer", 1402 | "description": "Allows only editing of content they created themselves", 1403 | "policies": [ 1404 | { 1405 | "effect": "allow", 1406 | "actions": [ 1407 | "create" 1408 | ], 1409 | "constraint": { 1410 | "and": [ 1411 | { 1412 | "equals": [ 1413 | { 1414 | "doc": "sys.type" 1415 | }, 1416 | "Entry" 1417 | ] 1418 | } 1419 | ] 1420 | } 1421 | }, 1422 | { 1423 | "effect": "allow", 1424 | "actions": [ 1425 | "create" 1426 | ], 1427 | "constraint": { 1428 | "and": [ 1429 | { 1430 | "equals": [ 1431 | { 1432 | "doc": "sys.type" 1433 | }, 1434 | "Asset" 1435 | ] 1436 | } 1437 | ] 1438 | } 1439 | }, 1440 | { 1441 | "effect": "allow", 1442 | "actions": [ 1443 | "read" 1444 | ], 1445 | "constraint": { 1446 | "and": [ 1447 | { 1448 | "equals": [ 1449 | { 1450 | "doc": "sys.type" 1451 | }, 1452 | "Entry" 1453 | ] 1454 | }, 1455 | { 1456 | "equals": [ 1457 | { 1458 | "doc": "sys.createdBy.sys.id" 1459 | }, 1460 | "User.current()" 1461 | ] 1462 | } 1463 | ] 1464 | } 1465 | }, 1466 | { 1467 | "effect": "allow", 1468 | "actions": [ 1469 | "update" 1470 | ], 1471 | "constraint": { 1472 | "and": [ 1473 | { 1474 | "equals": [ 1475 | { 1476 | "doc": "sys.type" 1477 | }, 1478 | "Entry" 1479 | ] 1480 | }, 1481 | { 1482 | "equals": [ 1483 | { 1484 | "doc": "sys.createdBy.sys.id" 1485 | }, 1486 | "User.current()" 1487 | ] 1488 | }, 1489 | { 1490 | "paths": [ 1491 | { 1492 | "doc": "fields.%.%" 1493 | } 1494 | ] 1495 | } 1496 | ] 1497 | } 1498 | }, 1499 | { 1500 | "effect": "allow", 1501 | "actions": [ 1502 | "delete" 1503 | ], 1504 | "constraint": { 1505 | "and": [ 1506 | { 1507 | "equals": [ 1508 | { 1509 | "doc": "sys.type" 1510 | }, 1511 | "Entry" 1512 | ] 1513 | }, 1514 | { 1515 | "equals": [ 1516 | { 1517 | "doc": "sys.createdBy.sys.id" 1518 | }, 1519 | "User.current()" 1520 | ] 1521 | } 1522 | ] 1523 | } 1524 | }, 1525 | { 1526 | "effect": "allow", 1527 | "actions": [ 1528 | "read" 1529 | ], 1530 | "constraint": { 1531 | "and": [ 1532 | { 1533 | "equals": [ 1534 | { 1535 | "doc": "sys.type" 1536 | }, 1537 | "Asset" 1538 | ] 1539 | }, 1540 | { 1541 | "equals": [ 1542 | { 1543 | "doc": "sys.createdBy.sys.id" 1544 | }, 1545 | "User.current()" 1546 | ] 1547 | } 1548 | ] 1549 | } 1550 | }, 1551 | { 1552 | "effect": "allow", 1553 | "actions": [ 1554 | "update" 1555 | ], 1556 | "constraint": { 1557 | "and": [ 1558 | { 1559 | "equals": [ 1560 | { 1561 | "doc": "sys.type" 1562 | }, 1563 | "Asset" 1564 | ] 1565 | }, 1566 | { 1567 | "equals": [ 1568 | { 1569 | "doc": "sys.createdBy.sys.id" 1570 | }, 1571 | "User.current()" 1572 | ] 1573 | }, 1574 | { 1575 | "paths": [ 1576 | { 1577 | "doc": "fields.%.%" 1578 | } 1579 | ] 1580 | } 1581 | ] 1582 | } 1583 | }, 1584 | { 1585 | "effect": "allow", 1586 | "actions": [ 1587 | "delete" 1588 | ], 1589 | "constraint": { 1590 | "and": [ 1591 | { 1592 | "equals": [ 1593 | { 1594 | "doc": "sys.type" 1595 | }, 1596 | "Asset" 1597 | ] 1598 | }, 1599 | { 1600 | "equals": [ 1601 | { 1602 | "doc": "sys.createdBy.sys.id" 1603 | }, 1604 | "User.current()" 1605 | ] 1606 | } 1607 | ] 1608 | } 1609 | } 1610 | ], 1611 | "permissions": { 1612 | "ContentModel": [ 1613 | "read" 1614 | ], 1615 | "Settings": [], 1616 | "ContentDelivery": [] 1617 | }, 1618 | "sys": { 1619 | "type": "Role", 1620 | "id": "3BfBcsMyyk2vTHgSvVE95c", 1621 | "version": 0, 1622 | "space": { 1623 | "sys": { 1624 | "type": "Link", 1625 | "linkType": "Space", 1626 | "id": "28p9vvm1oxuw" 1627 | } 1628 | }, 1629 | "createdBy": { 1630 | "sys": { 1631 | "type": "Link", 1632 | "linkType": "User", 1633 | "id": "066RqBikAjzKy0SWUEtFvH" 1634 | } 1635 | }, 1636 | "createdAt": "2017-05-11T12:01:17Z", 1637 | "updatedBy": { 1638 | "sys": { 1639 | "type": "Link", 1640 | "linkType": "User", 1641 | "id": "066RqBikAjzKy0SWUEtFvH" 1642 | } 1643 | }, 1644 | "updatedAt": "2017-05-11T12:01:17Z" 1645 | } 1646 | }, 1647 | { 1648 | "name": "Translator 1", 1649 | "description": "Allows editing of localized fields in the specified language", 1650 | "policies": [ 1651 | { 1652 | "effect": "allow", 1653 | "actions": [ 1654 | "read" 1655 | ], 1656 | "constraint": { 1657 | "and": [ 1658 | { 1659 | "equals": [ 1660 | { 1661 | "doc": "sys.type" 1662 | }, 1663 | "Entry" 1664 | ] 1665 | } 1666 | ] 1667 | } 1668 | }, 1669 | { 1670 | "effect": "allow", 1671 | "actions": [ 1672 | "read" 1673 | ], 1674 | "constraint": { 1675 | "and": [ 1676 | { 1677 | "equals": [ 1678 | { 1679 | "doc": "sys.type" 1680 | }, 1681 | "Asset" 1682 | ] 1683 | } 1684 | ] 1685 | } 1686 | }, 1687 | { 1688 | "effect": "allow", 1689 | "actions": [ 1690 | "update" 1691 | ], 1692 | "constraint": { 1693 | "and": [ 1694 | { 1695 | "equals": [ 1696 | { 1697 | "doc": "sys.type" 1698 | }, 1699 | "Entry" 1700 | ] 1701 | }, 1702 | { 1703 | "paths": [ 1704 | { 1705 | "doc": "fields.%.%" 1706 | } 1707 | ] 1708 | } 1709 | ] 1710 | } 1711 | }, 1712 | { 1713 | "effect": "allow", 1714 | "actions": [ 1715 | "update" 1716 | ], 1717 | "constraint": { 1718 | "and": [ 1719 | { 1720 | "equals": [ 1721 | { 1722 | "doc": "sys.type" 1723 | }, 1724 | "Asset" 1725 | ] 1726 | }, 1727 | { 1728 | "paths": [ 1729 | { 1730 | "doc": "fields.%.%" 1731 | } 1732 | ] 1733 | } 1734 | ] 1735 | } 1736 | } 1737 | ], 1738 | "permissions": { 1739 | "ContentModel": [ 1740 | "read" 1741 | ], 1742 | "Settings": [], 1743 | "ContentDelivery": [] 1744 | }, 1745 | "sys": { 1746 | "type": "Role", 1747 | "id": "3BgX7gCjpTS6ToMGo7tN6m", 1748 | "version": 0, 1749 | "space": { 1750 | "sys": { 1751 | "type": "Link", 1752 | "linkType": "Space", 1753 | "id": "28p9vvm1oxuw" 1754 | } 1755 | }, 1756 | "createdBy": { 1757 | "sys": { 1758 | "type": "Link", 1759 | "linkType": "User", 1760 | "id": "066RqBikAjzKy0SWUEtFvH" 1761 | } 1762 | }, 1763 | "createdAt": "2017-05-11T12:01:17Z", 1764 | "updatedBy": { 1765 | "sys": { 1766 | "type": "Link", 1767 | "linkType": "User", 1768 | "id": "066RqBikAjzKy0SWUEtFvH" 1769 | } 1770 | }, 1771 | "updatedAt": "2017-05-11T12:01:17Z" 1772 | } 1773 | }, 1774 | { 1775 | "name": "Translator 2", 1776 | "description": "Allows editing of localized fields in the specified language", 1777 | "policies": [ 1778 | { 1779 | "effect": "allow", 1780 | "actions": [ 1781 | "read" 1782 | ], 1783 | "constraint": { 1784 | "and": [ 1785 | { 1786 | "equals": [ 1787 | { 1788 | "doc": "sys.type" 1789 | }, 1790 | "Entry" 1791 | ] 1792 | } 1793 | ] 1794 | } 1795 | }, 1796 | { 1797 | "effect": "allow", 1798 | "actions": [ 1799 | "read" 1800 | ], 1801 | "constraint": { 1802 | "and": [ 1803 | { 1804 | "equals": [ 1805 | { 1806 | "doc": "sys.type" 1807 | }, 1808 | "Asset" 1809 | ] 1810 | } 1811 | ] 1812 | } 1813 | }, 1814 | { 1815 | "effect": "allow", 1816 | "actions": [ 1817 | "update" 1818 | ], 1819 | "constraint": { 1820 | "and": [ 1821 | { 1822 | "equals": [ 1823 | { 1824 | "doc": "sys.type" 1825 | }, 1826 | "Entry" 1827 | ] 1828 | }, 1829 | { 1830 | "paths": [ 1831 | { 1832 | "doc": "fields.%.%" 1833 | } 1834 | ] 1835 | } 1836 | ] 1837 | } 1838 | }, 1839 | { 1840 | "effect": "allow", 1841 | "actions": [ 1842 | "update" 1843 | ], 1844 | "constraint": { 1845 | "and": [ 1846 | { 1847 | "equals": [ 1848 | { 1849 | "doc": "sys.type" 1850 | }, 1851 | "Asset" 1852 | ] 1853 | }, 1854 | { 1855 | "paths": [ 1856 | { 1857 | "doc": "fields.%.%" 1858 | } 1859 | ] 1860 | } 1861 | ] 1862 | } 1863 | } 1864 | ], 1865 | "permissions": { 1866 | "ContentModel": [ 1867 | "read" 1868 | ], 1869 | "Settings": [], 1870 | "ContentDelivery": [] 1871 | }, 1872 | "sys": { 1873 | "type": "Role", 1874 | "id": "3Bi3CWAo8KktAQXSv6vl66", 1875 | "version": 0, 1876 | "space": { 1877 | "sys": { 1878 | "type": "Link", 1879 | "linkType": "Space", 1880 | "id": "28p9vvm1oxuw" 1881 | } 1882 | }, 1883 | "createdBy": { 1884 | "sys": { 1885 | "type": "Link", 1886 | "linkType": "User", 1887 | "id": "066RqBikAjzKy0SWUEtFvH" 1888 | } 1889 | }, 1890 | "createdAt": "2017-05-11T12:01:17Z", 1891 | "updatedBy": { 1892 | "sys": { 1893 | "type": "Link", 1894 | "linkType": "User", 1895 | "id": "066RqBikAjzKy0SWUEtFvH" 1896 | } 1897 | }, 1898 | "updatedAt": "2017-05-11T12:01:17Z" 1899 | } 1900 | }, 1901 | { 1902 | "name": "Translator 3", 1903 | "description": "Allows editing of localized fields in the specified language", 1904 | "policies": [ 1905 | { 1906 | "effect": "allow", 1907 | "actions": [ 1908 | "read" 1909 | ], 1910 | "constraint": { 1911 | "and": [ 1912 | { 1913 | "equals": [ 1914 | { 1915 | "doc": "sys.type" 1916 | }, 1917 | "Entry" 1918 | ] 1919 | } 1920 | ] 1921 | } 1922 | }, 1923 | { 1924 | "effect": "allow", 1925 | "actions": [ 1926 | "read" 1927 | ], 1928 | "constraint": { 1929 | "and": [ 1930 | { 1931 | "equals": [ 1932 | { 1933 | "doc": "sys.type" 1934 | }, 1935 | "Asset" 1936 | ] 1937 | } 1938 | ] 1939 | } 1940 | }, 1941 | { 1942 | "effect": "allow", 1943 | "actions": [ 1944 | "update" 1945 | ], 1946 | "constraint": { 1947 | "and": [ 1948 | { 1949 | "equals": [ 1950 | { 1951 | "doc": "sys.type" 1952 | }, 1953 | "Entry" 1954 | ] 1955 | }, 1956 | { 1957 | "paths": [ 1958 | { 1959 | "doc": "fields.%.%" 1960 | } 1961 | ] 1962 | } 1963 | ] 1964 | } 1965 | }, 1966 | { 1967 | "effect": "allow", 1968 | "actions": [ 1969 | "update" 1970 | ], 1971 | "constraint": { 1972 | "and": [ 1973 | { 1974 | "equals": [ 1975 | { 1976 | "doc": "sys.type" 1977 | }, 1978 | "Asset" 1979 | ] 1980 | }, 1981 | { 1982 | "paths": [ 1983 | { 1984 | "doc": "fields.%.%" 1985 | } 1986 | ] 1987 | } 1988 | ] 1989 | } 1990 | } 1991 | ], 1992 | "permissions": { 1993 | "ContentModel": [ 1994 | "read" 1995 | ], 1996 | "Settings": [], 1997 | "ContentDelivery": [] 1998 | }, 1999 | "sys": { 2000 | "type": "Role", 2001 | "id": "3BjfQSC3N7sqbZuUQmRC2a", 2002 | "version": 0, 2003 | "space": { 2004 | "sys": { 2005 | "type": "Link", 2006 | "linkType": "Space", 2007 | "id": "28p9vvm1oxuw" 2008 | } 2009 | }, 2010 | "createdBy": { 2011 | "sys": { 2012 | "type": "Link", 2013 | "linkType": "User", 2014 | "id": "066RqBikAjzKy0SWUEtFvH" 2015 | } 2016 | }, 2017 | "createdAt": "2017-05-11T12:01:17Z", 2018 | "updatedBy": { 2019 | "sys": { 2020 | "type": "Link", 2021 | "linkType": "User", 2022 | "id": "066RqBikAjzKy0SWUEtFvH" 2023 | } 2024 | }, 2025 | "updatedAt": "2017-05-11T12:01:17Z" 2026 | } 2027 | } 2028 | ] 2029 | } 2030 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: `.env.${process.env.NODE_ENV}`, 3 | }) 4 | 5 | const contentfulConfig = { 6 | spaceId: process.env.CONTENTFUL_SPACE_ID, 7 | accessToken: process.env.CONTENTFUL_ACCESS_TOKEN, 8 | } 9 | 10 | // if you want to use the preview API please define 11 | // CONTENTFUL_HOST in your environment config 12 | // the `host` property should map to `preview.contentful.com` 13 | // https://www.contentful.com/developers/docs/references/content-preview-api/#/reference/spaces/space/get-a-space/console/js 14 | if (process.env.CONTENTFUL_HOST) { 15 | contentfulConfig.host = process.env.CONTENTFUL_HOST 16 | } 17 | 18 | const { spaceId, accessToken } = contentfulConfig 19 | 20 | if (!spaceId || !accessToken) { 21 | throw new Error( 22 | 'Contentful spaceId and the access token need to be provided.' 23 | ) 24 | } 25 | 26 | module.exports = { 27 | siteMetadata: { 28 | title: 'Gatsby Contentful starter', 29 | }, 30 | pathPrefix: '/gatsby-contentful-starter', 31 | plugins: [ 32 | 'gatsby-transformer-remark', 33 | 'gatsby-transformer-sharp', 34 | 'gatsby-plugin-react-helmet', 35 | 'gatsby-plugin-sharp', 36 | { 37 | resolve: 'gatsby-source-contentful', 38 | options: contentfulConfig, 39 | }, 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird') 2 | const path = require('path') 3 | 4 | exports.createPages = ({ graphql, actions }) => { 5 | const { createPage } = actions 6 | 7 | return new Promise((resolve, reject) => { 8 | const blogPost = path.resolve('./src/templates/blog-post.js') 9 | resolve( 10 | graphql( 11 | ` 12 | { 13 | allContentfulBlogPost { 14 | edges { 15 | node { 16 | title 17 | slug 18 | } 19 | } 20 | } 21 | } 22 | ` 23 | ).then(result => { 24 | if (result.errors) { 25 | console.log(result.errors) 26 | reject(result.errors) 27 | } 28 | 29 | const posts = result.data.allContentfulBlogPost.edges 30 | posts.forEach((post, index) => { 31 | createPage({ 32 | path: `/blog/${post.node.slug}/`, 33 | component: blogPost, 34 | context: { 35 | slug: post.node.slug 36 | }, 37 | }) 38 | }) 39 | }) 40 | ) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-contentful-starter", 3 | "description": "Starter Contentful Gatsby Blog", 4 | "version": "1.0.0", 5 | "author": "Stefan Judis ", 6 | "bugs": { 7 | "url": "https://github.com/contentful-userland/gatsby-contentful-starter/issues" 8 | }, 9 | "dependencies": { 10 | "gatsby-image": "^2.3.4", 11 | "gatsby-plugin-react-helmet": "^3.2.4", 12 | "gatsby-plugin-sharp": "^2.5.6", 13 | "gatsby-source-contentful": "^2.2.9", 14 | "gatsby-transformer-remark": "^2.7.3", 15 | "gatsby-transformer-sharp": "^2.4.6", 16 | "lodash": "^4.17.15", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1", 19 | "react-helmet": "^6.0.0", 20 | "sharp": "^0.25.2" 21 | }, 22 | "devDependencies": { 23 | "babel-eslint": "^10.1.0", 24 | "chalk": "^4.0.0", 25 | "contentful-import": "^7.7.8", 26 | "dotenv": "^8.2.0", 27 | "eslint": "^6.8.0", 28 | "eslint-plugin-react": "^7.19.0", 29 | "gatsby": "^2.20.25", 30 | "gh-pages": "^2.2.0", 31 | "inquirer": "^7.1.0", 32 | "prettier": "^2.0.4", 33 | "rimraf": "^3.0.2", 34 | "yargs-parser": "^18.1.3" 35 | }, 36 | "engines": { 37 | "node": ">=10.13.0" 38 | }, 39 | "homepage": "https://github.com/contentful-userland/gatsby-contentful-starter#readme", 40 | "keywords": [ 41 | "gatsby", 42 | "contentful" 43 | ], 44 | "license": "MIT", 45 | "main": "n/a", 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/contentful-userland/gatsby-contentful-starter.git" 49 | }, 50 | "scripts": { 51 | "dev": "npm run develop", 52 | "develop": "gatsby develop", 53 | "lint": "eslint --ext .js,.jsx --ignore-pattern public .", 54 | "test": "echo \"Error: no test specified\" && exit 1", 55 | "format": "prettier --trailing-comma es5 --no-semi --single-quote --write 'src/**/*.js' 'src/**/*.md' 'bin/*.js'", 56 | "build": "gatsby build", 57 | "deploy": "gatsby build --prefix-paths && gh-pages -d public", 58 | "fix-semi": "eslint --quiet --ignore-pattern node_modules --ignore-pattern public --parser babel-eslint --no-eslintrc --rule '{\"semi\": [2, \"never\"], \"no-extra-semi\": [2]}' --fix *.js bin/*.js", 59 | "postinstall": "node ./bin/hello.js", 60 | "setup": "node ./bin/setup.js", 61 | "start": "npm run develop", 62 | "heroku-postbuild": "gatsby build", 63 | "cleanup-repository": "yarn remove contentful-import chalk inquirer && node ./bin/cleanup.js && rimraf bin contentful" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful-userland/gatsby-contentful-starter/51f961f061bb5a32ab279bd04df135f7898a5c3f/screenshot.jpg -------------------------------------------------------------------------------- /setup.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful-userland/gatsby-contentful-starter/51f961f061bb5a32ab279bd04df135f7898a5c3f/setup.jpg -------------------------------------------------------------------------------- /src/components/article-preview.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import Img from 'gatsby-image' 4 | 5 | import styles from './article-preview.module.css' 6 | 7 | export default ({ article }) => ( 8 |
9 | 10 |

11 | {article.title} 12 |

13 | {article.publishDate} 14 |
19 | {article.tags && 20 | article.tags.map(tag => ( 21 |

22 | {tag} 23 |

24 | ))} 25 |
26 | ) 27 | -------------------------------------------------------------------------------- /src/components/article-preview.module.css: -------------------------------------------------------------------------------- 1 | .previewTitle { 2 | font-size: 1.5em; 3 | } 4 | 5 | .tag { 6 | color: #A0A0A0; 7 | text-decoration: none; 8 | display: inline-block; 9 | padding: .33333rem .5rem; 10 | line-height: 1; 11 | border-radius: 2px; 12 | border: 1px solid #A0A0A0; 13 | margin-right: .5em; 14 | } -------------------------------------------------------------------------------- /src/components/base.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Avenir"; 3 | font-weight: 400; 4 | font-style: normal; 5 | src: url("/avenir-400.woff2") format("woff2"); 6 | font-display: swap; 7 | } 8 | 9 | body { 10 | font-family: "Avenir", Tahoma, Arial, Helvetica, sans-serif; 11 | font-size: 1em; 12 | line-height: 1.65; 13 | color: #373F49; 14 | background: #eee; 15 | margin: 0; 16 | } 17 | 18 | img { 19 | display: block; 20 | width: 100%; 21 | } 22 | 23 | h1, 24 | h2, 25 | h3 { 26 | font-size: 2em; 27 | font-weight: normal; 28 | } 29 | 30 | a { 31 | color: currentColor; 32 | } 33 | 34 | .wrapper { 35 | width: calc(100% - 10vmin); 36 | margin: 0 auto; 37 | padding: 5vmin 0; 38 | } 39 | 40 | /** 41 | * article grid 42 | */ 43 | .article-list { 44 | margin: 0; 45 | padding: 0; 46 | list-style: none; 47 | display: grid; 48 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 49 | grid-gap: 5vmin; 50 | } 51 | 52 | /** 53 | * 54 | */ 55 | .section-headline { 56 | padding: 0 0 0.4em 0; 57 | margin: 0 0 5vmin 0; 58 | border-bottom: 1px solid #ddd; 59 | } 60 | 61 | /** 62 | * 63 | */ 64 | .list-inline { 65 | margin: 0; 66 | padding: 0; 67 | list-style: none; 68 | } 69 | 70 | .list-inline li { 71 | display: inline-block; 72 | } 73 | -------------------------------------------------------------------------------- /src/components/container.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default ({ children }) => ( 4 |
{children}
5 | ) 6 | -------------------------------------------------------------------------------- /src/components/hero.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Img from 'gatsby-image' 3 | 4 | import styles from './hero.module.css' 5 | 6 | export default ({ data }) => ( 7 |
8 | {data.name} 13 |
14 |

{data.name}

15 |

{data.title}

16 |

{data.shortBio.shortBio}

17 |
18 |
19 | ) 20 | -------------------------------------------------------------------------------- /src/components/hero.module.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | position: relative; 3 | background: #000; 4 | color: #fff; 5 | text-align: center; 6 | } 7 | 8 | .heroImage { 9 | /* 10 | Ensure golden ratio for the hero size while limiting it to some extend to 11 | the viewport width 12 | */ 13 | height: 61.8vh; 14 | max-height: 400px; 15 | } 16 | 17 | .heroDetails { 18 | position: absolute; 19 | background: rgba(0, 0, 0, 0.7); 20 | left: 50%; 21 | bottom: 0; 22 | transform: translate(-50%, 0); 23 | font-size: 14px; 24 | padding: 0 0.5em; 25 | } 26 | 27 | @media(min-width: 600px) { 28 | .heroDetails { 29 | font-size: 16px; 30 | } 31 | } 32 | 33 | @media(min-width: 1000px) { 34 | .heroDetails { 35 | font-size: 20px; 36 | } 37 | } 38 | 39 | .heroHeadline { 40 | margin: 0; 41 | } 42 | 43 | .heroTitle { 44 | margin: 0; 45 | font-size: 1.125em; 46 | font-weight: bold; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import base from './base.css' 4 | import Container from './container' 5 | import Navigation from './navigation' 6 | 7 | class Template extends React.Component { 8 | render() { 9 | const { location, children } = this.props 10 | let header 11 | 12 | let rootPath = `/` 13 | if (typeof __PREFIX_PATHS__ !== `undefined` && __PREFIX_PATHS__) { 14 | rootPath = __PATH_PREFIX__ + `/` 15 | } 16 | 17 | return ( 18 | 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | } 25 | 26 | export default Template 27 | -------------------------------------------------------------------------------- /src/components/navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'gatsby' 3 | import styles from './navigation.module.css' 4 | 5 | export default () => ( 6 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/components/navigation.module.css: -------------------------------------------------------------------------------- 1 | .navigation { 2 | display: flex; 3 | justify-content: center; 4 | list-style: none; 5 | padding: 0; 6 | margin: 0; 7 | height: 20vh; 8 | max-height: 100px; 9 | font-size: 1.25em; 10 | } 11 | 12 | .navigationItem { 13 | display: inline-flex; 14 | align-items: center; 15 | margin: 0 1em; 16 | } 17 | 18 | .navigationItem a { 19 | color: currentColor; 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/blog.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, graphql } from 'gatsby' 3 | import get from 'lodash/get' 4 | import { Helmet } from 'react-helmet' 5 | import styles from './blog.module.css' 6 | import Layout from '../components/layout' 7 | import ArticlePreview from '../components/article-preview' 8 | 9 | class BlogIndex extends React.Component { 10 | render() { 11 | const siteTitle = get(this, 'props.data.site.siteMetadata.title') 12 | const posts = get(this, 'props.data.allContentfulBlogPost.edges') 13 | 14 | return ( 15 | 16 |
17 | 18 |
Blog
19 |
20 |

Recent articles

21 |
    22 | {posts.map(({ node }) => { 23 | return ( 24 |
  • 25 | 26 |
  • 27 | ) 28 | })} 29 |
30 |
31 |
32 |
33 | ) 34 | } 35 | } 36 | 37 | export default BlogIndex 38 | 39 | export const pageQuery = graphql` 40 | query BlogIndexQuery { 41 | site { 42 | siteMetadata { 43 | title 44 | } 45 | } 46 | allContentfulBlogPost(sort: { fields: [publishDate], order: DESC }) { 47 | edges { 48 | node { 49 | title 50 | slug 51 | publishDate(formatString: "MMMM Do, YYYY") 52 | tags 53 | heroImage { 54 | fluid(maxWidth: 350, maxHeight: 196, resizingBehavior: SCALE) { 55 | ...GatsbyContentfulFluid_tracedSVG 56 | } 57 | } 58 | description { 59 | childMarkdownRemark { 60 | html 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | ` 68 | -------------------------------------------------------------------------------- /src/pages/blog.module.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | height: 61.8vh; 6 | max-height: 400px; 7 | background: #e1e1e1; 8 | font-size: 2em; 9 | overflow: hidden; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import get from 'lodash/get' 4 | import { Helmet } from 'react-helmet' 5 | import Hero from '../components/hero' 6 | import Layout from '../components/layout' 7 | import ArticlePreview from '../components/article-preview' 8 | 9 | class RootIndex extends React.Component { 10 | render() { 11 | const siteTitle = get(this, 'props.data.site.siteMetadata.title') 12 | const posts = get(this, 'props.data.allContentfulBlogPost.edges') 13 | const [author] = get(this, 'props.data.allContentfulPerson.edges') 14 | 15 | return ( 16 | 17 |
18 | 19 | 20 |
21 |

Recent articles

22 |
    23 | {posts.map(({ node }) => { 24 | return ( 25 |
  • 26 | 27 |
  • 28 | ) 29 | })} 30 |
31 |
32 |
33 |
34 | ) 35 | } 36 | } 37 | 38 | export default RootIndex 39 | 40 | export const pageQuery = graphql` 41 | query HomeQuery { 42 | site { 43 | siteMetadata { 44 | title 45 | } 46 | } 47 | allContentfulBlogPost(sort: { fields: [publishDate], order: DESC }) { 48 | edges { 49 | node { 50 | title 51 | slug 52 | publishDate(formatString: "MMMM Do, YYYY") 53 | tags 54 | heroImage { 55 | fluid(maxWidth: 350, maxHeight: 196, resizingBehavior: SCALE) { 56 | ...GatsbyContentfulFluid_tracedSVG 57 | } 58 | } 59 | description { 60 | childMarkdownRemark { 61 | html 62 | } 63 | } 64 | } 65 | } 66 | } 67 | allContentfulPerson( 68 | filter: { contentful_id: { eq: "15jwOBqpxqSAOy2eOO4S0m" } } 69 | ) { 70 | edges { 71 | node { 72 | name 73 | shortBio { 74 | shortBio 75 | } 76 | title 77 | heroImage: image { 78 | fluid( 79 | maxWidth: 1180 80 | maxHeight: 480 81 | resizingBehavior: PAD 82 | background: "rgb:000000" 83 | ) { 84 | ...GatsbyContentfulFluid_tracedSVG 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | ` 92 | -------------------------------------------------------------------------------- /src/templates/blog-post.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { graphql } from 'gatsby' 3 | import { Helmet } from 'react-helmet' 4 | import get from 'lodash/get' 5 | import Img from 'gatsby-image' 6 | import Layout from '../components/layout' 7 | 8 | import heroStyles from '../components/hero.module.css' 9 | 10 | class BlogPostTemplate extends React.Component { 11 | render() { 12 | const post = get(this.props, 'data.contentfulBlogPost') 13 | const siteTitle = get(this.props, 'data.site.siteMetadata.title') 14 | 15 | return ( 16 | 17 |
18 | 19 |
20 | {post.title} 25 |
26 |
27 |

{post.title}

28 |

33 | {post.publishDate} 34 |

35 |
40 |
41 |
42 | 43 | ) 44 | } 45 | } 46 | 47 | export default BlogPostTemplate 48 | 49 | export const pageQuery = graphql` 50 | query BlogPostBySlug($slug: String!) { 51 | site { 52 | siteMetadata { 53 | title 54 | } 55 | } 56 | contentfulBlogPost(slug: { eq: $slug }) { 57 | title 58 | publishDate(formatString: "MMMM Do, YYYY") 59 | heroImage { 60 | fluid(maxWidth: 1180, background: "rgb:000000") { 61 | ...GatsbyContentfulFluid_tracedSVG 62 | } 63 | } 64 | body { 65 | childMarkdownRemark { 66 | html 67 | } 68 | } 69 | } 70 | } 71 | ` 72 | -------------------------------------------------------------------------------- /src/templates/blog-post.module.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | 6 | height: 12.5em; 7 | background: #e1e1e1; 8 | margin: -1em -2.5em 1em; 9 | font-size: 2em 10 | } -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "public/", 3 | "headers": { 4 | "/**.js": { 5 | "Cache-Control": "public, max-age=0, must-revalidate" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /static/avenir-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful-userland/gatsby-contentful-starter/51f961f061bb5a32ab279bd04df135f7898a5c3f/static/avenir-400.woff2 -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful-userland/gatsby-contentful-starter/51f961f061bb5a32ab279bd04df135f7898a5c3f/static/favicon.ico -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | --------------------------------------------------------------------------------