├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── test-lint.yml ├── .gitignore ├── .prettierrc.js ├── .vscode └── settings.json ├── README.md ├── bin └── setup │ └── contentful.js ├── components ├── __snapshots__ │ ├── footer.spec.tsx.snap │ ├── nav.spec.tsx.snap │ ├── not-found.spec.tsx.snap │ └── subscription.spec.tsx.snap ├── footer.spec.tsx ├── footer.tsx ├── index.ts ├── nav.spec.tsx ├── nav.tsx ├── not-found.spec.tsx ├── not-found.tsx ├── subscription.spec.tsx └── subscription.tsx ├── config.json ├── config └── index.ts ├── core ├── api.spec.ts ├── api.ts ├── index.ts ├── integrations │ ├── contentful.service.ts │ ├── index.ts │ └── typings.ts ├── models │ ├── author.ts │ ├── category.ts │ ├── index.ts │ └── post.ts └── typings │ └── index.ts ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── now.json ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _error.tsx ├── category │ └── [id].tsx ├── index.tsx └── post │ └── [id].tsx ├── public ├── favicon.ico └── zeit.svg ├── schemas └── contentful.json ├── setup-contentful.png ├── styles ├── bootstrap-user-variables.scss └── main.scss ├── tests └── setupTests.ts ├── tsconfig.jest.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2020, 5 | sourceType: 'module', 6 | ecmaFeatures: { 7 | jsx: true 8 | } 9 | }, 10 | settings: { 11 | react: { 12 | version: 'detect' 13 | } 14 | }, 15 | extends: [ 16 | 'plugin:react/recommended', 17 | 'plugin:@typescript-eslint/recommended', 18 | 'prettier/@typescript-eslint', 19 | 'plugin:prettier/recommended', 20 | 'plugin:jsx-a11y/recommended', 21 | ], 22 | rules: { 23 | 'react/react-in-jsx-scope': 'off' 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/test-lint.yml: -------------------------------------------------------------------------------- 1 | name: Test & Lint 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | types: [ opened, synchronize ] 8 | 9 | jobs: 10 | dependencies: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: yarn install --frozen-lockfile --check-files 15 | - uses: actions/cache@v1 16 | id: cache-dependencies 17 | with: 18 | path: '.' 19 | key: ${{ github.sha }} 20 | 21 | test-lint: 22 | runs-on: ubuntu-latest 23 | needs: dependencies 24 | steps: 25 | - uses: actions/cache@v1 26 | id: restore-dependencies 27 | with: 28 | path: '.' 29 | key: ${{ github.sha }} 30 | - run: yarn test 31 | - run: yarn lint 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | .now 28 | .vercel -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: 'all', 4 | singleQuote: true, 5 | printWidth: 120, 6 | tabWidth: 2, 7 | }; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "CONTENTFUL" 4 | ] 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Medium style boilerplate blog 2 | 3 | > This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app). 4 | 5 | ## Data Sources 6 | 7 | This project has been designed to support any data source, under the `core/` folder you can find the models and service structure. 8 | 9 | ### Contentful 10 | 11 | Contentful is the default integration supported at the moment, we also provided a setup script together with a schema to easily get it up and running. 12 | 13 | ## Template 14 | 15 | This project uses [Mudana](https://www.wowthemes.net/mundana-free-html-bootstrap-template/) to achieve the medium style blog. 16 | 17 | ## Getting Started 18 | 19 | ### Install dependencies 20 | 21 | ``` 22 | $ git@github.com:maxigimenez/next-medium-blog-boilerplate.git 23 | $ yarn install 24 | ``` 25 | 26 | ### Setup models 27 | 28 | #### Contentful 29 | 30 | This projects comes with a Contentful schema ready to be used. Using `yarn setup:contentful`: 31 | 32 | ![](./setup-contentful.png) 33 | 34 | This command will ask you for a space ID, and access tokens for the Contentful Management and Delivery API and then import the schema defined on "schemas/contentful.json". 35 | 36 | Once the script is done you will be able to launch the blog and see dummy information ready to be changed. 37 | 38 | ## Scripts 39 | 40 | ### `yarn dev` 41 | 42 | Run the project locally. Then open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 43 | 44 | ## Deploy 45 | 46 | The repository comes with a simple `now.json` configuration, so we recommend to use [Zeit.co](https://zeit.co) to host the blog. 47 | -------------------------------------------------------------------------------- /bin/setup/contentful.js: -------------------------------------------------------------------------------- 1 | const spaceImport = require('contentful-import'); 2 | const schema = require('../../schemas/contentful.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 | You can find all the needed information in your Contentful space under: 14 | ${chalk.yellow( 15 | `app.contentful.com ${chalk.red('->')} Space Settings ${chalk.red( 16 | '->' 17 | )} API keys` 18 | )} 19 | The ${chalk.green('Content Management API Token')} 20 | will be used to import and write data to your space. 21 | The ${chalk.green('Content Delivery API Token')} 22 | will be used to ship published production-ready content in your Gatsby app. 23 | The ${chalk.green('Content Preview API Token')} 24 | will be used to show not published data in your development environment. 25 | Ready? Let's do it! 🎉 26 | `); 27 | 28 | const questions = [ 29 | { 30 | name: 'spaceId', 31 | message: 'Your Space ID', 32 | when: !argv.spaceId && !process.env.CONTENTFUL_SPACE_ID, 33 | validate: input => 34 | /^[a-z0-9]{12}$/.test(input) || 35 | 'Space ID must be 12 lowercase characters', 36 | }, 37 | { 38 | name: 'managementToken', 39 | when: !argv.managementToken && !process.env.CONTENTFUL_MANAGEMENT_TOKEN, 40 | message: 'Your Content Management API access token', 41 | }, 42 | { 43 | name: 'accessToken', 44 | when: !argv.accessToken && !process.env.CONTENTFUL_ACCESS_TOKEN, 45 | message: 'Your Content Delivery API access token', 46 | } 47 | ]; 48 | 49 | inquirer 50 | .prompt(questions) 51 | .then(({ spaceId, managementToken, accessToken }) => { 52 | const { 53 | CONTENTFUL_SPACE_ID, 54 | CONTENTFUL_ACCESS_TOKEN, 55 | CONTENTFUL_MANAGEMENT_TOKEN 56 | } = process.env; 57 | 58 | // env vars are given precedence followed by args provided to the setup 59 | // followed by input given to prompts displayed by the setup script 60 | spaceId = CONTENTFUL_SPACE_ID || argv.spaceId || spaceId; 61 | managementToken = CONTENTFUL_MANAGEMENT_TOKEN || argv.managementToken || managementToken; 62 | accessToken = CONTENTFUL_ACCESS_TOKEN || argv.accessToken || accessToken; 63 | 64 | console.log('Writing config file...'); 65 | const configFiles = [`.env`].map(file => 66 | path.join(__dirname, '../..', file) 67 | ); 68 | 69 | const fileContents = 70 | [ 71 | `# Do NOT commit this file to source control`, 72 | `CONTENTFUL_SPACE_ID=${spaceId}`, 73 | `CONTENTFUL_ACCESS_TOKEN=${accessToken}`, 74 | ].join('\n') + '\n'; 75 | 76 | configFiles.forEach(file => { 77 | writeFileSync(file, fileContents, 'utf8') 78 | console.log(`Config file ${chalk.yellow(file)} written`); 79 | }) 80 | return { spaceId, managementToken }; 81 | }) 82 | .then(({ spaceId, managementToken }) => 83 | spaceImport({ spaceId, managementToken, content: schema }) 84 | ) 85 | .then((_, error) => { 86 | console.log( 87 | `All set! You can now run ${chalk.yellow( 88 | 'yarn dev' 89 | )} to see it in action.` 90 | ); 91 | }) 92 | .catch(error => console.error(error)); 93 | -------------------------------------------------------------------------------- /components/__snapshots__/footer.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`footer snapshots render 1`] = ` 4 |
7 | 45 |
46 | `; 47 | -------------------------------------------------------------------------------- /components/__snapshots__/nav.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`nav snapshots render if categories undefined 1`] = ` 4 | 61 | `; 62 | 63 | exports[`nav snapshots render with categories 1`] = ` 64 | 152 | `; 153 | 154 | exports[`nav snapshots render without categories 1`] = ` 155 | 212 | `; 213 | -------------------------------------------------------------------------------- /components/__snapshots__/not-found.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`not-found snapshots render default 1`] = ` 4 | 5 | 6 | 7 | Demo Blog - Next.js medium style boilerplate 8 | 9 | 10 |
18 |

21 | 404 22 |

23 |
26 |

30 | The page you requested was not found. 31 |

32 |
33 |
34 |
35 | `; 36 | -------------------------------------------------------------------------------- /components/__snapshots__/subscription.spec.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`subscription snapshots render default 1`] = ` 4 |
7 |
10 |
13 |
16 | Become a member 17 |
18 | Get the latest news right in your inbox. We never spam! 19 |
20 |
23 |
26 |
29 | 37 |
38 |
41 | 49 |
50 |
51 |
52 |
53 |
54 | `; 55 | -------------------------------------------------------------------------------- /components/footer.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { Footer } from './footer'; 4 | 5 | describe('footer', () => { 6 | describe('snapshots', () => { 7 | test('render', () => { 8 | const component = shallow(