├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── content ├── blog │ └── 2019-01-25-hello.md └── mdx │ ├── another-entry.mdx │ └── hello.mdx ├── gatsby-browser.js ├── gatsby-config.js ├── gatsby-node.js ├── gatsby-ssr.js ├── package.json ├── src ├── Boot.tsx ├── Theme.jsx ├── cms │ └── cms.jsx ├── components │ ├── NavTree │ │ ├── NavState.tsx │ │ ├── __tests__ │ │ │ └── NavTree.test.tsx │ │ ├── index.tsx │ │ └── utils │ │ │ └── pathListToTree.ts │ └── ui │ │ └── Typography.tsx ├── layouts │ └── DefaultLayout.tsx ├── pages │ └── index.tsx └── utils │ └── playgroundHastPlugin.js ├── static ├── admin │ └── config.yml ├── assets │ └── media │ │ └── logo.svg └── favicon.ico ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | module.exports = { 4 | parser: "@typescript-eslint/parser", 5 | plugins: ["@typescript-eslint", "graphql"], 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier", 11 | ], 12 | parserOptions: { 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | ecmaVersion: 6, 17 | project: "./tsconfig.json", 18 | sourceType: "module", 19 | }, 20 | settings: { 21 | react: { 22 | version: "detect", 23 | }, 24 | }, 25 | env: { 26 | es6: true, 27 | browser: true, 28 | jest: true, 29 | node: true, 30 | }, 31 | rules: { 32 | "@typescript-eslint/explicit-function-return-type": "off", 33 | "@typescript-eslint/explicit-member-accessibility": "off", 34 | "@typescript-eslint/indent": "off", 35 | "@typescript-eslint/member-delimiter-style": "off", 36 | "@typescript-eslint/no-explicit-any": "off", 37 | "@typescript-eslint/no-var-requires": "off", 38 | "@typescript-eslint/no-use-before-define": "off", 39 | "@typescript-eslint/no-unused-vars": [ 40 | "error", 41 | { 42 | argsIgnorePattern: "^_", 43 | }, 44 | ], 45 | "graphql/template-strings": [ 46 | "error", 47 | { 48 | env: "relay", 49 | schemaJsonFilepath: path.resolve(__dirname, "./schema.json"), 50 | tagName: "graphql", 51 | }, 52 | ], 53 | "no-console": [ 54 | "error", 55 | { 56 | allow: ["warn", "error"], 57 | }, 58 | ], 59 | "react/display-name": 0, 60 | "react/prop-types": 0, 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .yarn-integrity 4 | yarn-debug.log 5 | yarn-error.log 6 | 7 | node_modules/ 8 | typings/ 9 | 10 | # Gatsby files 11 | .cache/ 12 | public 13 | schema.json 14 | schema.graphql 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | ".git/": true, 4 | "node_modules/": false, 5 | "public/": false 6 | }, 7 | "search.exclude": { 8 | "**/.cache": true, 9 | "**/node_modules": true, 10 | "**/public": true 11 | }, 12 | "editor.rulers": [80], 13 | "editor.tabSize": 2, 14 | "editor.formatOnSave": true, 15 | "eslint.autoFixOnSave": true, 16 | "eslint.validate": [{ 17 | "language": "javascript", 18 | "autoFix": true 19 | }, 20 | { 21 | "language": "javascriptreact", 22 | "autoFix": true 23 | }, 24 | { 25 | "language": "typescript", 26 | "autoFix": true 27 | }, 28 | { 29 | "language": "typescriptreact", 30 | "autoFix": true 31 | } 32 | ], 33 | "tslint.enable": false, 34 | "typescript.tsdk": "./node_modules/typescript/lib", 35 | "debug.node.autoAttach": "on" 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Christopher Pappas 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Barebones Gatsby Starter 2 | 3 | - [TypeScript](https://www.typescriptlang.org/) 4 | - [Styled Components](https://www.styled-components.com/) 5 | - [Rebass](https://github.com/rebassjs/rebass) 6 | - [MDX](https://mdxjs.com/) 7 | - [Jest](https://jestjs.io/) 8 | - [NetlifyCMS](https://www.netlifycms.org/), with local file-system and MDX 9 | support 10 | - [Lint Staged](https://github.com/okonet/lint-staged) 11 | - [Prettier](https://prettier.io/) 12 | 13 | This starter was extracted from an earlier iteration I completed while working 14 | on [Palette](https://palette.artsy.net), Artsy's design system. Check out the 15 | [docs site](https://palette.artsy.net/) for a more full-featured example 16 | (including some interesting live-coding capabilities) or the 17 | [source-code](https://github.com/artsy/palette). 18 | 19 | ### Development 20 | 21 | ```sh 22 | yarn start 23 | yarn clean 24 | yarn type-check 25 | ``` 26 | 27 | > Please note that Yarn is required, as NPM may not install dependencies correctly, causing issues for development. 28 | 29 | ### Deployment 30 | 31 | ```sh 32 | yarn build 33 | ``` 34 | 35 | ### Demo 36 | 37 | ![demo](https://user-images.githubusercontent.com/236943/51792565-abd93e00-2167-11e9-8bcb-87f7dccece52.gif) 38 | 39 | ### GraphQL Validation 40 | 41 | Validation has been added via `eslint-plugin-graphql`: 42 | 43 | screen shot 2019-01-26 at 6 49 01 pm 44 | 45 | **TODO:** Figure out how to generate usable types for TypeScript with 46 | `apollo-cli`. 47 | 48 |
49 | Gatsby API Details 50 | 51 | 1. **`gatsby-browser.js`**: This file is where Gatsby expects to find any usage 52 | of the [Gatsby browser APIs](https://www.gatsbyjs.org/docs/browser-apis/) 53 | (if any). These allow customization/extension of default Gatsby settings 54 | affecting the browser. 55 | 1. **`gatsby-config.js`**: This is the main configuration file for a Gatsby 56 | site. This is where you can specify information about your site (metadata) 57 | like the site title and description, which Gatsby plugins you’d like to 58 | include, etc. (Check out the 59 | [config docs](https://www.gatsbyjs.org/docs/gatsby-config/) for more 60 | detail). 61 | 1. **`gatsby-node.js`**: This file is where Gatsby expects to find any usage of 62 | the [Gatsby Node APIs](https://www.gatsbyjs.org/docs/node-apis/) (if any). 63 | These allow customization/extension of default Gatsby settings affecting 64 | pieces of the site build process. 65 | 1. **`gatsby-ssr.js`**: This file is where Gatsby expects to find any usage of 66 | the 67 | [Gatsby server-side rendering APIs](https://www.gatsbyjs.org/docs/ssr-apis/) 68 | (if any). These allow customization of default Gatsby settings affecting 69 | server-side rendering. 70 | 71 |
72 | -------------------------------------------------------------------------------- /content/blog/2019-01-25-hello.md: -------------------------------------------------------------------------------- 1 | --- 2 | templateKey: blog-post 3 | title: Hello 4 | date: 2019-01-26T05:15:24.699Z 5 | description: 'How are you? ' 6 | tags: 7 | - and 8 | - some 9 | - tags 10 | --- 11 | Some content 12 | -------------------------------------------------------------------------------- /content/mdx/another-entry.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Another entry 3 | --- 4 | 5 | 6 | ### And it works :) 7 | -------------------------------------------------------------------------------- /content/mdx/hello.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Edit mdx in NetlifyCMS! ' 3 | --- 4 | ### This 5 | 6 |

is some jsx

7 | 8 | 9 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Implement Gatsby's Browser APIs in this file. 5 | * 6 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 7 | */ 8 | 9 | import { Boot } from "./src/Boot" 10 | 11 | export const wrapRootElement = Boot 12 | -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const playgroundHastPlugin = require("./src/utils/playgroundHastPlugin") 4 | 5 | module.exports = { 6 | siteMetadata: { 7 | title: "Gatsby Starter", 8 | description: "-- placeholder --", 9 | author: "damassi.pappas@gmail.com", 10 | }, 11 | plugins: [ 12 | { 13 | resolve: "gatsby-mdx", 14 | options: { 15 | extensions: [".mdx", ".md"], 16 | 17 | // Default layouts are meta wrappers around .mdx pages. Can be useful to 18 | // share queries across different types of pages. 19 | defaultLayouts: { 20 | default: require.resolve("./src/layouts/DefaultLayout.tsx"), 21 | }, 22 | 23 | // MDX AST transformers 24 | hastPlugins: [playgroundHastPlugin], 25 | 26 | // Imports here are available globally to .mdx files, with the exception 27 | // of automatically created pages located in /pages. This is a bug in 28 | // gatsby-mdx. See https://github.com/ChristopherBiscardi/gatsby-mdx/issues/243 29 | // 30 | // Also note: For mdx to work in NetlifyCMS, global scope passed in here 31 | // also be passed into `cms.js`, under the `scope` key. 32 | // 33 | globalScope: ` 34 | import { UIComponents } from 'Theme' 35 | export default { 36 | ...UIComponents 37 | } 38 | `, 39 | 40 | // mdPlugins: [], 41 | // gatsbyRemarkPlugins: [{}], 42 | }, 43 | }, 44 | { 45 | resolve: "gatsby-plugin-netlify-cms", 46 | options: { 47 | modulePath: `${__dirname}/src/cms/cms.jsx`, 48 | enableIdentityWidget: false, 49 | publicPath: "admin", 50 | htmlTitle: "Admin", 51 | manualInit: true, 52 | }, 53 | }, 54 | { 55 | resolve: "gatsby-source-filesystem", 56 | options: { 57 | name: "blog", 58 | path: `${__dirname}/content/blog/`, 59 | }, 60 | }, 61 | { 62 | resolve: "gatsby-source-filesystem", 63 | options: { 64 | name: "mdx", 65 | path: `${__dirname}/content/mdx/`, 66 | }, 67 | }, 68 | "gatsby-plugin-catch-links", 69 | "gatsby-plugin-styled-components", 70 | "gatsby-plugin-typescript", 71 | "gatsby-transformer-sharp", 72 | "gatsby-plugin-sharp", 73 | ], 74 | } 75 | -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Implement Gatsby's Node APIs in this file. 5 | * 6 | * See: https://www.gatsbyjs.org/docs/node-apis/ 7 | */ 8 | 9 | const write = require("write") 10 | const WebpackNotifierPlugin = require("webpack-notifier") 11 | const path = require("path") 12 | const { toLower } = require("lodash") 13 | const { introspectionQuery, graphql, printSchema } = require("gatsby/graphql") 14 | const { createFilePath } = require("gatsby-source-filesystem") 15 | 16 | // TODO: Fix apollo type generation 17 | // const WebpackShellPlugin = require("webpack-shell-plugin") 18 | 19 | /** 20 | * Intercept and modify the GraphQL schema 21 | */ 22 | exports.onCreateNode = ({ node, getNode, actions }) => { 23 | if (node.internal.type === "Mdx") { 24 | const route = toLower( 25 | createFilePath({ 26 | node, 27 | getNode, 28 | trailingSlash: false, 29 | }) 30 | ) 31 | 32 | // Add a new field -- route -- which can be accessed from the schema under 33 | // fields { route }. 34 | actions.createNodeField({ 35 | node, 36 | name: "route", 37 | value: route, 38 | }) 39 | } 40 | } 41 | 42 | /** 43 | * Dynamically create pages for all .mdx content. 44 | * 45 | * NOTE: Content located in /pages is created automatically but should be limited 46 | * to static pages like "About" or "Home", etc, and is subject data limitations 47 | * since query data resolved below cannot be injected in at build time. 48 | */ 49 | exports.createPages = ({ graphql, actions }) => { 50 | return new Promise((resolve, reject) => { 51 | resolve( 52 | graphql(` 53 | query CreatePagesQuery { 54 | allMdx { 55 | edges { 56 | node { 57 | id 58 | fields { 59 | route 60 | } 61 | } 62 | } 63 | } 64 | } 65 | `).then(result => { 66 | if (result.errors) { 67 | console.error(result.errors) 68 | reject(result.errors) 69 | } 70 | 71 | result.data.allMdx.edges.forEach(({ node }) => { 72 | actions.createPage({ 73 | // Encode the route 74 | path: node.fields.route, 75 | // Layout for the page 76 | component: path.resolve("./src/layouts/DefaultLayout.tsx"), 77 | // Values defined here are injected into the page as props and can 78 | // be passed to a GraphQL query as arguments 79 | context: { 80 | id: node.id, 81 | }, 82 | }) 83 | }) 84 | }) 85 | ) 86 | }) 87 | } 88 | 89 | /** 90 | * Add the file-system as an api proxy: 91 | * https://www.gatsbyjs.org/docs/api-proxy/#advanced-proxying 92 | */ 93 | exports.onCreateDevServer = ({ app }) => { 94 | const fsMiddlewareAPI = require("netlify-cms-backend-fs/dist/fs") 95 | fsMiddlewareAPI(app) 96 | } 97 | 98 | /** 99 | * Update default Webpack configuration 100 | */ 101 | exports.onCreateWebpackConfig = ({ actions }) => { 102 | actions.setWebpackConfig({ 103 | plugins: [ 104 | new WebpackNotifierPlugin({ 105 | skipFirstNotification: true, 106 | }), 107 | 108 | // FIXME: Investigate Apollo error 109 | // new WebpackShellPlugin({ 110 | // onBuildEnd: ["yarn emit-graphql-types"], 111 | // }), 112 | ], 113 | resolve: { 114 | // Enable absolute import paths 115 | modules: [path.resolve(__dirname, "src"), "node_modules"], 116 | }, 117 | }) 118 | } 119 | 120 | /** 121 | * Generate GraphQL schema.json file to be read by tslint 122 | * Thanks: https://gist.github.com/kkemple/6169e8dc16369b7c01ad7408fc7917a9 123 | */ 124 | exports.onPostBootstrap = async ({ store }) => { 125 | try { 126 | const { schema } = store.getState() 127 | const jsonSchema = await graphql(schema, introspectionQuery) 128 | const sdlSchema = printSchema(schema) 129 | 130 | write.sync("schema.json", JSON.stringify(jsonSchema.data), {}) 131 | write.sync("schema.graphql", sdlSchema, {}) 132 | 133 | console.log("\n\n[gatsby-plugin-extract-schema] Wrote schema\n") // eslint-disable-line 134 | } catch (error) { 135 | console.error( 136 | "\n\n[gatsby-plugin-extract-schema] Failed to write schema: ", 137 | error, 138 | "\n" 139 | ) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 5 | * 6 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 7 | */ 8 | 9 | import { Boot } from "./src/Boot" 10 | 11 | export const wrapRootElement = Boot 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-typescript-rebass-netlifycms", 3 | "description": "My standard gatsby setup", 4 | "version": "0.1.0", 5 | "scripts": { 6 | "build": "gatsby build", 7 | "clean": "rm -rf .cache && yarn start", 8 | "develop": "nodemon --exec \"gatsby develop\"", 9 | "debug": "nodemon --exec \"node --inspect-brk --no-lazy node_modules/.bin/gatsby develop\"", 10 | "lint": "eslint . --ext ts --ext tsx", 11 | "prettier-write": "prettier --write \"src/**/*.js\"", 12 | "release": "auto shipit", 13 | "start": "yarn develop", 14 | "test": "echo \"Write tests! -> https://gatsby.app/unit-testing\"", 15 | "type-check": "tsc --pretty --noEmit" 16 | }, 17 | "husky": { 18 | "hooks": { 19 | "pre-commit": "lint-staged", 20 | "pre-push": "yarn run type-check" 21 | } 22 | }, 23 | "lint-staged": { 24 | "*.@(ts|tsx)": [ 25 | "yarn lint", 26 | "yarn prettier-write", 27 | "git add" 28 | ] 29 | }, 30 | "nodemonConfig": { 31 | "ignore": [ 32 | ".cache/*", 33 | "public/*", 34 | "schema.json", 35 | "src/*" 36 | ], 37 | "delay": "1500" 38 | }, 39 | "prettier": { 40 | "bracketSpacing": true, 41 | "proseWrap": "always", 42 | "semi": false, 43 | "singleQuote": false, 44 | "trailingComma": "es5" 45 | }, 46 | "devDependencies": { 47 | "@types/jest": "^23.3.13", 48 | "@types/lodash": "^4.14.120", 49 | "@types/react": "^16.7.20", 50 | "@types/react-dom": "^16.0.11", 51 | "@types/rebass": "^0.3.4", 52 | "@types/styled-components": "^4.1.6", 53 | "@typescript-eslint/eslint-plugin": "^1.1.0", 54 | "auto-release-cli": "2.3.0", 55 | "babel-eslint": "^10.0.1", 56 | "eslint": "^5.12.1", 57 | "eslint-config-prettier": "^3.6.0", 58 | "eslint-plugin-graphql": "^3.0.1", 59 | "eslint-plugin-react": "^7.12.4", 60 | "gatsby-plugin-typescript": "^2.0.3", 61 | "graphql-typescript-definitions": "^0.15.0", 62 | "husky": "^1.3.1", 63 | "jest": "^24.0.0", 64 | "lint-staged": "^8.1.0", 65 | "multer": "^1.4.1", 66 | "netlify-cms-backend-fs": "^0.3.6", 67 | "nodemon": "^1.18.9", 68 | "prettier": "^1.15.3", 69 | "typescript": "^3.2.4", 70 | "typescript-styled-plugin": "^0.13.0", 71 | "webpack-notifier": "^1.7.0", 72 | "webpack-shell-plugin": "^0.5.0" 73 | }, 74 | "dependencies": { 75 | "@babel/core": "^7.2.2", 76 | "@mdx-js/mdx": "^0.16.6", 77 | "@mdx-js/tag": "^0.16.6", 78 | "@reach/router": "^1.2.1", 79 | "babel-plugin-styled-components": "^1.10.0", 80 | "d3-ease": "^1.0.5", 81 | "docz-utils": "^0.13.6", 82 | "gatsby": "^2.0.76", 83 | "gatsby-image": "^2.0.20", 84 | "gatsby-mdx": "^0.3.4", 85 | "gatsby-plugin-catch-links": "^2.0.9", 86 | "gatsby-plugin-manifest": "^2.0.9", 87 | "gatsby-plugin-netlify-cms": "^3.0.10", 88 | "gatsby-plugin-offline": "^2.0.16", 89 | "gatsby-plugin-sharp": "^2.0.14", 90 | "gatsby-plugin-styled-components": "^3.0.4", 91 | "gatsby-source-filesystem": "^2.0.16", 92 | "gatsby-transformer-sharp": "^2.1.8", 93 | "hast-util-to-string": "^1.0.1", 94 | "lodash": "^4.17.11", 95 | "marked": "^0.6.0", 96 | "netlify-cms": "^2.3.3", 97 | "netlify-cms-widget-mdx": "^0.3.1", 98 | "prismjs": "^1.15.0", 99 | "prop-types": "^15.6.2", 100 | "react": "^16.7.0", 101 | "react-dom": "^16.7.0", 102 | "react-head": "^3.0.2", 103 | "react-powerplug": "^1.0.0", 104 | "rebass": "^3.0.1", 105 | "styled-components": "^4.1.3", 106 | "styled-system": "^3.2.1", 107 | "unstated": "^2.1.1", 108 | "write": "^1.0.3" 109 | }, 110 | "license": "MIT", 111 | "publishConfig": { 112 | "registry": "https://registry.npmjs.org/" 113 | }, 114 | "repository": { 115 | "type": "git", 116 | "url": "https://github.com/damassi/gatsby-starter-typescript-rebass-netlifycms" 117 | }, 118 | "bugs": { 119 | "url": "https://github.com/damassi/gatsby-starter-typescript-rebass-netlifycms/issues" 120 | }, 121 | "authors": [ 122 | "Christopher Pappas = ({ element }) => { 10 | return ( 11 | 12 | 13 | {element} 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/Theme.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * Since this file is shared with NetlifyCMS it must be .jsx 5 | */ 6 | 7 | import React, { Fragment } from "react" 8 | import styled, { ThemeProvider, createGlobalStyle } from "styled-components" 9 | import { Button } from "rebass" 10 | 11 | export const theme = { 12 | // TODO: https://rebassjs.org/theming 13 | } 14 | 15 | const GlobalStyle = createGlobalStyle` 16 | html, body { 17 | font-family: Arial, Helvetica, sans-serif; 18 | } 19 | 20 | h3 { 21 | font-family: Arial, Helvetica, sans-serif 22 | } 23 | ` 24 | 25 | export const LayoutComponents = { 26 | h1: styled.h1` 27 | font-size: 20px; 28 | `, 29 | p: styled.p` 30 | font-size: 16px; 31 | `, 32 | } 33 | 34 | export const UIComponents = { 35 | Button: props => , 36 | } 37 | 38 | export const Theme = ({ children }) => ( 39 | 40 | 41 | 42 | {children} 43 | 44 | 45 | ) 46 | -------------------------------------------------------------------------------- /src/cms/cms.jsx: -------------------------------------------------------------------------------- 1 | import { MdxControl, MdxPreview } from "netlify-cms-widget-mdx" 2 | import React, { Component } from "react" 3 | import { StyleSheetManager } from "styled-components" 4 | import { Theme, LayoutComponents, UIComponents } from "../Theme" 5 | import { FileSystemBackend } from "netlify-cms-backend-fs" 6 | import CMS, { init } from "netlify-cms" 7 | 8 | const isClient = typeof window !== "undefined" 9 | const isDevelopment = process.env.NODE_ENV === "development" 10 | 11 | if (isClient) { 12 | window.CMS_MANUAL_INIT = true 13 | } 14 | 15 | if (isDevelopment) { 16 | // Allows for local development overrides in cms.yaml 17 | window.CMS_ENV = "localhost_development" 18 | 19 | // Attach to the file system 20 | CMS.registerBackend("file-system", FileSystemBackend) 21 | } 22 | 23 | // @ts-check 24 | 25 | // Custom components need refs for validation and thus must be a class. 26 | // Additionally, after , only one child is allowed. 27 | // See https://github.com/netlify/netlify-cms/issues/1346 28 | 29 | class MDXWidget extends Component { 30 | render() { 31 | return ( 32 | 33 | 34 | 35 | ) 36 | } 37 | } 38 | 39 | // The preview window which renders MDX content. 40 | // Docs: https://www.netlifycms.org/docs/customization/ 41 | 42 | const PreviewWindow = props => { 43 | const iframe = document.getElementsByTagName("iframe")[0] 44 | const iframeHeadElem = iframe.contentDocument.head 45 | 46 | const mdxProps = { 47 | // This key represents html elements used in markdown; h1, p, etc 48 | components: LayoutComponents, 49 | // Pass components used in the editor (and shared throughout mdx) here: 50 | scope: UIComponents, 51 | 52 | mdPlugins: [], 53 | } 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | 64 | // Netlify collections that set `widget: mdx` will be able to use this custom 65 | // widget. NOTE: The StyleSheet manager can *only* be injected into the Preview. 66 | // Docs: https://www.netlifycms.org/docs/widgets/ 67 | 68 | CMS.registerWidget("mdx", MDXWidget, PreviewWindow) 69 | 70 | // Start the CMS 71 | init() 72 | -------------------------------------------------------------------------------- /src/components/NavTree/NavState.tsx: -------------------------------------------------------------------------------- 1 | import { includes, without } from "lodash" 2 | import { Container } from "unstated" 3 | 4 | export interface State { 5 | expandedNavItems: string[] 6 | } 7 | 8 | export class NavState extends Container { 9 | state = { 10 | expandedNavItems: [], 11 | } 12 | 13 | constructor() { 14 | super() 15 | 16 | /** 17 | * At the moment the NavTree will only ever have a depth of 2, so grab the 18 | * pathname and extract the first segment. This is so we can expand a nav 19 | * list if deep linking directly into a sub-nav item. 20 | * 21 | * `/foo` => 'foo' 22 | * `/foo/bar` => 'bar' 23 | * 24 | * TODO: 25 | * Figure out how to get `location.pathname` from the router rather 26 | * than the window. I think this will be problematic when we build the app 27 | * for prod due to SSR rendering. 28 | * 29 | * import { Location } from '@reach/router` will return it. 30 | */ 31 | if (typeof window !== "undefined") { 32 | const expandedNavItems = ["/" + window.location.pathname.split("/")[1]] 33 | this.state = { 34 | expandedNavItems, 35 | } 36 | } 37 | } 38 | 39 | toggleNavItem = navItem => { 40 | const { expandedNavItems } = this.state 41 | const hasNavItem = includes(expandedNavItems, navItem) 42 | 43 | const updated = hasNavItem 44 | ? without(expandedNavItems, navItem) 45 | : expandedNavItems.concat([navItem]) 46 | 47 | this.setState({ 48 | expandedNavItems: updated, 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/NavTree/__tests__/NavTree.test.tsx: -------------------------------------------------------------------------------- 1 | describe("NavTree", () => { 2 | it("should work", () => { 3 | expect("it to work") 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /src/components/NavTree/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | import { Subscribe } from "unstated" 3 | import { Sans } from "components/ui/Typography" 4 | import React, { Fragment } from "react" 5 | import { TreeNode, pathListToTree } from "./utils/pathListToTree" 6 | import { NavState } from "./NavState" 7 | import { Link, StaticQuery, graphql } from "gatsby" 8 | import { get, includes, reject, sortBy } from "lodash" 9 | import { Box } from "rebass" 10 | 11 | export const NavTree = _props => { 12 | return ( 13 | { 33 | return renderNavTree(buildNavTree(data)) 34 | }} 35 | /> 36 | ) 37 | } 38 | 39 | function renderNavTree(tree: TreeNode[], treeDepth: number = 0) { 40 | const getTreeLayout = () => { 41 | if (treeDepth > 0) { 42 | return { 43 | ml: 2, 44 | py: 0.3, 45 | size: "2", 46 | } 47 | } else { 48 | return { 49 | ml: 0, 50 | py: 0.2, 51 | size: "3", 52 | } 53 | } 54 | } 55 | 56 | const { ml, py, size } = getTreeLayout() 57 | 58 | return ( 59 | 60 | {(navState: NavState) => ( 61 | 62 | {tree.map(({ data, children, formattedName, path }: TreeNode) => { 63 | const hasChildren = Boolean(children.length) 64 | const navSpacer = get(data, "navSpacer", {}) 65 | 66 | switch (hasChildren) { 67 | case true: { 68 | treeDepth++ 69 | const expanded = includes(navState.state.expandedNavItems, path) 70 | 71 | return ( 72 | 73 | 74 | {/* 75 | Don't navigate, just toggle subnav open and closed 76 | */} 77 | { 81 | navState.toggleNavItem(path) 82 | 83 | // Recompute tree since subnav could be open or closed 84 | treeDepth = 0 85 | }} 86 | > 87 | {formattedName} 88 | 89 | 90 | {expanded && renderNavTree(children, treeDepth)} 91 | 92 | ) 93 | } 94 | case false: { 95 | return ( 96 | 97 | 98 | {formattedName} 99 | 100 | 101 | ) 102 | } 103 | } 104 | })} 105 | 106 | )} 107 | 108 | ) 109 | } 110 | 111 | // TODO: Add type once Apollo generator is fixed 112 | function buildNavTree(data) { 113 | const routes = data.allMdx.edges.reduce((acc, { node }) => { 114 | const { route } = node.fields 115 | if (route.length) { 116 | return [ 117 | ...acc, 118 | { 119 | path: route, 120 | data: node.frontmatter, 121 | }, 122 | ] 123 | } else { 124 | return acc 125 | } 126 | }, []) 127 | 128 | // Perform various operations depending on frontmatter 129 | const sorted = sortBy(routes, route => route.data.order) 130 | const visible = reject(sorted, route => route.data.hideInNav) 131 | const navTree = pathListToTree(visible).map(path => path.children)[0] 132 | return navTree 133 | } 134 | 135 | const NavLinkWrapper = ({ 136 | className, 137 | children, 138 | disableNavigation, 139 | to, 140 | ...props 141 | }) => { 142 | /** 143 | * If a nav item is disabled and has children it will toggle its children 144 | * open and closed, but not navigate. If we want parent nav items to show 145 | * their own page *and* toggle, set this prop to false. 146 | */ 147 | if (disableNavigation) { 148 | return ( 149 | 150 | {children} 151 | 152 | ) 153 | } else { 154 | return ( 155 | 156 | {children} 157 | 158 | ) 159 | } 160 | } 161 | 162 | const NavLink = styled(NavLinkWrapper)` 163 | cursor: pointer; 164 | 165 | &&.isActive { 166 | color: purple; 167 | 168 | &:after { 169 | content: " * "; 170 | } 171 | 172 | &:hover { 173 | text-decoration: underline; 174 | color: purple; 175 | } 176 | } 177 | ` 178 | -------------------------------------------------------------------------------- /src/components/NavTree/utils/pathListToTree.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/pthm/node-path-list-to-tree. Thanks! 2 | 3 | /** 4 | * Takes an array of path-like strings and creates a tree from the result. 5 | * 6 | * @example 7 | * 8 | * const paths = [{path: 'foo' }, {path: 'foo/bar', data: { hello: 'world' }}] 9 | * const tree = pathListToTree(paths) 10 | * // => [{ 11 | * name: 'foo', 12 | * formattedName: 'Foo', 13 | * path: 'foo', 14 | * data: undefined 15 | * children: [ 16 | * { 17 | * name: 'bar', 18 | * formattedName: 'Bar', 19 | * path: 'foo/bar', 20 | * data: { hello: 'world' } 21 | * } 22 | * ] 23 | * ] 24 | * }] 25 | */ 26 | 27 | interface PathListProps { 28 | path: string 29 | data: object 30 | } 31 | 32 | export function pathListToTree(pathList: PathListProps[]): TreeNode[] { 33 | const tree: TreeNode[] = [] 34 | for (const { path, data } of pathList) { 35 | const split: string[] = path.split("/") 36 | createNode(split, tree, path, data) 37 | } 38 | return tree 39 | } 40 | 41 | export interface TreeNode { 42 | name: string 43 | formattedName: string 44 | path: string 45 | data: any 46 | children: TreeNode[] 47 | } 48 | 49 | function createNode( 50 | path: string[], 51 | tree: TreeNode[], 52 | fullPath?: string, 53 | data?: any 54 | ): void { 55 | const name = path.shift() 56 | const idx = tree.findIndex((e: TreeNode) => { 57 | return e.name === name 58 | }) 59 | 60 | if (idx < 0) { 61 | tree.push({ 62 | name, 63 | // TODO: Pass in transformer callback 64 | formattedName: data.title, 65 | path: fullPath, 66 | data, 67 | children: [], 68 | }) 69 | if (path.length !== 0) { 70 | createNode(path, tree[tree.length - 1].children, fullPath, data) 71 | } 72 | } else { 73 | createNode(path, tree[idx].children, fullPath, data) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/components/ui/Typography.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components" 2 | 3 | interface Typography { 4 | size?: string 5 | } 6 | 7 | export const Sans = styled.div` 8 | font-family: arial; 9 | ` 10 | 11 | export const Serif = styled.div` 12 | font-family: "Times New Roman", Times, serif; 13 | ` 14 | -------------------------------------------------------------------------------- /src/layouts/DefaultLayout.tsx: -------------------------------------------------------------------------------- 1 | import MDXRenderer from "gatsby-mdx/mdx-renderer" 2 | import React from "react" 3 | import { Box, Flex } from "rebass" 4 | import { Link, graphql } from "gatsby" 5 | import { Sans, Serif } from "components/ui/Typography" 6 | import { NavTree } from "components/NavTree" 7 | 8 | export default function DocsLayout(props) { 9 | const { 10 | data: { 11 | mdx: { 12 | code, 13 | frontmatter: { title }, 14 | }, 15 | }, 16 | } = props 17 | 18 | return ( 19 | 20 | 21 | 22 | 23 | Home 24 | 25 | 26 | 27 | 28 | 29 | {title} 30 | {code.body} 31 | 32 | 33 | ) 34 | } 35 | 36 | /** 37 | * Query for data for the page. Note that $id is injected in via context from 38 | * actions.createPage. See gatsby-node.js for more info. 39 | */ 40 | export const pageQuery = graphql` 41 | query DocsLayoutQuery($id: String) { 42 | mdx(id: { eq: $id }) { 43 | id 44 | frontmatter { 45 | title 46 | } 47 | code { 48 | body 49 | } 50 | } 51 | } 52 | ` 53 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { graphql, Link, StaticQuery } from "gatsby" 2 | import React from "react" 3 | 4 | export default function Home() { 5 | return ( 6 | { 24 | return ( 25 |
26 |

Page Index

27 |
28 | {data.allMdx.edges.map((edge, index) => { 29 | const { 30 | node: { 31 | fields: { route }, 32 | frontmatter: { title }, 33 | }, 34 | } = edge 35 | 36 | return ( 37 |
38 | {title} 39 |
40 | ) 41 | })} 42 |
43 |
44 | ) 45 | }} 46 | /> 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/playgroundHastPlugin.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const path = require("path") 4 | const prettier = require("prettier") 5 | const { jsx } = require("docz-utils") 6 | 7 | /** 8 | * This traverses the .mdx AST searching for a component and if 9 | * found, take its contents and inject them in as stringified code. This is so 10 | * we can avoid needing to use markdown code blocks in order to render an 11 | * interactive editor. 12 | * 13 | * NOTE: When working in this file a cache builds up in gatsby. Changes require 14 | * a boot using `yarn clean`. 15 | * 16 | * TODO: 17 | * - Don't hardcode component name 18 | * - Write test 19 | */ 20 | 21 | const COMPONENT_NAME = "Playground" 22 | 23 | // Matches the compnoent tag 24 | const tagRegex = new RegExp(`<[\/]{0,1}(${COMPONENT_NAME})[^><]*>`, "g") 25 | 26 | // Matches JSX attributes 27 | const attributesRegex = /([\w\-.:]+)\s*=\s*("[^"]*"|{[^]*}|'[^']*')/g 28 | 29 | module.exports = () => { 30 | return tree => { 31 | const jsxNodes = tree.children.filter(child => child.type === "jsx") 32 | 33 | jsxNodes.forEach(node => { 34 | // Iterate over JSX children looking for `` node 35 | const isPlayground = node.value.includes(`<${COMPONENT_NAME}`) 36 | if (!isPlayground) { 37 | return 38 | } 39 | 40 | // Get the playground tag 41 | const tag = node.value.match(tagRegex)[0] 42 | // Capture the props 43 | let props = tag.match(attributesRegex) 44 | props = props ? props.join("") : "" 45 | // Remove outer playground tag and capture contents 46 | const codeContents = node.value.replace(tagRegex, "") 47 | const code = prettifyCode(codeContents).substring(1) // remove leading ; 48 | const sanitized = jsx.sanitizeCode(code) 49 | node.value = "" 50 | }) 51 | 52 | return tree 53 | } 54 | } 55 | 56 | // Helpers 57 | 58 | const packagePath = path.resolve(__dirname, "../", "package.json") 59 | const prettierOptions = prettier.resolveConfig.sync(packagePath) 60 | const prettifyCode = code => { 61 | return prettier.format(code, { 62 | parser: "babylon", 63 | ...prettierOptions, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /static/admin/config.yml: -------------------------------------------------------------------------------- 1 | # Netlify "kitchen sink" example: https://github.com/netlify/netlify-cms/blob/master/dev-test/config.yml 2 | 3 | localhost_development: 4 | backend: 5 | name: file-system 6 | api_root: /api 7 | 8 | backend: 9 | name: github 10 | repo: damassi/gatsby-typescript-netlify-starter 11 | branch: master 12 | 13 | display_url: http://localhost:8000 14 | media_folder: static/assets 15 | public_folder: assets 16 | 17 | collections: 18 | - name: mdx 19 | label: Mdx Example 20 | description: Documentation 21 | folder: content/mdx 22 | extension: mdx 23 | format: frontmatter 24 | widget: mdx 25 | create: true 26 | fields: 27 | - { label: Title, name: title, widget: string, required: true } 28 | - { label: Body, name: body, widget: mdx } 29 | 30 | - name: blog 31 | label: Blog 32 | folder: content/blog 33 | create: true 34 | slug: "{{year}}-{{month}}-{{day}}-{{slug}}" 35 | fields: 36 | - { 37 | label: Template Key, 38 | name: templateKey, 39 | widget: hidden, 40 | default: blog-post, 41 | } 42 | - { label: Title, name: title, widget: string } 43 | - { label: Publish Date, name: date, widget: datetime } 44 | - { label: Description, name: description, widget: text } 45 | - { label: Tags, name: tags, widget: list } 46 | - { label: Body, name: body, widget: markdown } 47 | -------------------------------------------------------------------------------- /static/assets/media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damassi/gatsby-starter-typescript-rebass-netlifycms/94ced8f53d8c4805bc266dd7cdca8c399bac7ce9/static/favicon.ico -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "baseUrl": "./src", 6 | "jsx": "react", 7 | "lib": ["dom", "esnext", "es2015", "es2016", "es2017"], 8 | "moduleResolution": "node", 9 | "noImplicitAny": false, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "plugins": [ 13 | { 14 | "name": "typescript-styled-plugin" 15 | } 16 | ], 17 | "target": "es2015" 18 | }, 19 | "include": ["**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules/*", "public/*"] 21 | } 22 | --------------------------------------------------------------------------------