├── .circleci └── config.yml ├── .dependabot └── config.yml ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── semantic.yml └── workflows │ └── auto-approve.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── catalog-info.yaml ├── next-env.d.ts ├── next.config.js ├── package.json ├── postcss.config.js ├── public └── favicon │ ├── apple-icon-114x114.png │ ├── apple-icon-57x57.png │ ├── apple-icon-72x72.png │ └── favicon.png ├── src ├── components │ ├── RelatedPages.tsx │ ├── block-title.tsx │ ├── link.tsx │ ├── logo.tsx │ ├── mobile-navigation.tsx │ ├── page-head.tsx │ ├── page-link.tsx │ ├── preview-banner.tsx │ ├── renderer │ │ ├── block-renderer.tsx │ │ ├── help-center-article.tsx │ │ ├── hero.tsx │ │ ├── image.tsx │ │ ├── section.tsx │ │ ├── text.tsx │ │ └── video.tsx │ └── top-navigation.tsx ├── lib │ ├── api.ts │ ├── constants.ts │ ├── generated-types │ │ ├── TypeComponent_hero.ts │ │ ├── TypeComponent_image.ts │ │ ├── TypeComponent_section.ts │ │ ├── TypeComponent_text.ts │ │ ├── TypeComponent_video.ts │ │ ├── TypePage_help_center_article.ts │ │ ├── TypePage_landing.ts │ │ ├── TypeSeo.ts │ │ └── index.ts │ ├── pageParsers.ts │ ├── preview.ts │ ├── rich-text │ │ ├── embedded-asset.tsx │ │ ├── hyperlink.tsx │ │ ├── index.ts │ │ ├── render.tsx │ │ └── summary.ts │ ├── translations │ │ ├── context.tsx │ │ ├── getInitialLocale.ts │ │ ├── index.ts │ │ └── locales.ts │ ├── types.ts │ └── useNavigation.ts ├── pages │ ├── [locale] │ │ ├── [slug].tsx │ │ ├── articles │ │ │ └── [slug].tsx │ │ └── index.tsx │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx └── styles │ └── index.css ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | node: circleci/node@1.1.6 4 | 5 | commands: 6 | run_with_cache: 7 | parameters: 8 | steps: 9 | type: steps 10 | steps: 11 | - restore_cache: 12 | keys: 13 | - cache-v1 14 | - steps: << parameters.steps >> 15 | - save_cache: 16 | key: cache-v1 17 | paths: 18 | - ~/.cache/yarn 19 | 20 | jobs: 21 | lint: 22 | executor: 23 | name: node/default 24 | steps: 25 | - checkout 26 | - run_with_cache: 27 | steps: 28 | - run: yarn install --prefer-offline --pure-lockfile 29 | - run: yarn prettier:check 30 | - run: yarn lint 31 | - run: yarn tsc 32 | 33 | workflows: 34 | lint-and-test: 35 | jobs: 36 | - lint 37 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: 'javascript' 4 | directory: '/' 5 | update_schedule: 'weekly' 6 | target_branch: 'master' 7 | default_labels: 8 | - 'dependencies' 9 | - 'dependabot' 10 | commit_message: 11 | prefix: 'chore' 12 | automerged_updates: 13 | - match: 14 | dependency_type: 'development' 15 | update_type: 'semver:minor' 16 | - match: 17 | dependency_type: 'production' 18 | update_type: 'semver:patch' 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CF_SPACE_ID= 2 | CF_DELIVERY_ACCESS_TOKEN= 3 | CF_PREVIEW_ACCESS_TOKEN= 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # eslint ignores its own config file by default 2 | !.eslintrc.js 3 | # ignore nested node_modules 4 | **/node_modules/** 5 | src/lib/types 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const jsExtensions = ['.js', '.jsx']; 2 | const tsExtensions = ['.ts', '.tsx']; 3 | const allExtensions = jsExtensions.concat(tsExtensions); 4 | 5 | module.exports = { 6 | parser: '@typescript-eslint/parser', 7 | env: { 8 | node: true, 9 | browser: true, 10 | }, 11 | extends: [ 12 | 'eslint:recommended', 13 | 'plugin:jest/recommended', 14 | 'plugin:@typescript-eslint/eslint-recommended', 15 | 'plugin:@typescript-eslint/recommended', 16 | 'plugin:import/errors', 17 | 'plugin:import/warnings', 18 | 'plugin:react/recommended', 19 | 'prettier', 20 | ], 21 | parserOptions: { 22 | ecmaVersion: 6, 23 | sourceType: 'module', 24 | }, 25 | plugins: ['react-hooks', 'jest'], 26 | settings: { 27 | react: { 28 | version: '16.13.0', 29 | }, 30 | jest: { 31 | version: 26, 32 | }, 33 | 'import/extensions': allExtensions, 34 | 'import/parsers': { 35 | '@typescript-eslint/parser': tsExtensions, 36 | }, 37 | 'import/resolver': { 38 | typescript: {}, 39 | node: { 40 | extensions: allExtensions, 41 | }, 42 | }, 43 | }, 44 | rules: { 45 | '@typescript-eslint/naming-convention': [ 46 | 'warn', 47 | { 48 | selector: 'variable', 49 | format: ['camelCase'], 50 | }, 51 | ], 52 | '@typescript-eslint/no-use-before-define': 'off', 53 | '@typescript-eslint/explicit-function-return-type': 'off', 54 | 'react-hooks/rules-of-hooks': 'error', 55 | 'react-hooks/exhaustive-deps': 'error', 56 | 'react/display-name': 'off', 57 | // 'react/prop-types': ['error', { ignore: ['children'] }], 58 | 'react/jsx-no-target-blank': ['error', { enforceDynamicLinks: 'always' }], 59 | 60 | 'react/react-in-jsx-scope': 'off', 61 | 'react/prop-types': 'off', 62 | '@typescript-eslint/no-var-requires': 0, 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Short description 2 | 3 | 6 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | titleAndCommits: true 2 | anyCommit: true 3 | -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-approve 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | auto-approve: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: hmarr/auto-approve-action@v2.0.0 10 | if: github.actor == 'dependabot[bot]' || github.actor == 'dependabot-preview[bot]' 11 | with: 12 | github-token: '${{ secrets.GITHUB_TOKEN }}' 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .now 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | .idea 113 | 114 | # yarn v2 115 | 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .pnp.* 120 | 121 | .env.local -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v14.15 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "jsxBracketSameLine": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Contentful 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 | # Compose Starter: Help Center + Next.js 2 | 3 | This is a sample website frontend to help you get started with Compose and 4 | Next.js. You can use this example with Compose's quick start "Simple website" content model for empty 5 | spaces. 6 | 7 | ## Getting started 8 | 9 | ### Installing dependencies 10 | 11 | ``` 12 | yarn 13 | ``` 14 | 15 | ### Running locally 16 | 17 | Copy `.env.example` to `.env` and adapt the environment to your setup: 18 | 19 | - `CF_SPACE_ID`: The ID of a Compose compatible space to be used 20 | - `CF_DELIVERY_ACCESS_TOKEN`: A delivery API key for the space 21 | - `CF_PREVIEW_ACCESS_TOKEN`: A preview API key for the space 22 | 23 | and then 24 | 25 | ``` 26 | yarn run dev 27 | ``` 28 | 29 | to start the website on `http://localhost:3000` 30 | 31 | ## Deploy to Vercel 32 | 33 | You can use [Vercel](https://vercel.com/) to easily deploy the app by clicking the deploy button below: 34 | 35 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcontentful%2Fcompose-starter-helpcenter-nextjs&env=CF_SPACE_ID,CF_DELIVERY_ACCESS_TOKEN,CF_PREVIEW_ACCESS_TOKEN&envDescription=Space%20ID%20and%20API%20Keys%20needed%20for%20the%20frontend%20to%20access%20your%20Contentful%20Space&envLink=https%3A%2F%2Fapp.contentful.com%2Fdeeplink%3Flink%3Dapi&project-name=contentful-compose-helpcenter-starter&repo-name=contentful-compose-helpcenter-starter) 36 | 37 | For manual deployment, you can following the steps below: 38 | 39 | 1. Open your Vercel dashboard and click on "New project". 40 | 2. Click on "Import a Third-Party Git Repository" and enter the url of this repo. 41 | 3. Choose the Vercel scope where you want to deploy the app. 42 | - For example your personal scope. 43 | 4. Add the 3 environment variables in the project settings: 44 | - `CF_SPACE_ID`: The ID of a Compose compatible space to be used; 45 | - `CF_DELIVERY_ACCESS_TOKEN`: A delivery API key for the space; 46 | - `CF_PREVIEW_ACCESS_TOKEN`: A preview API key for the space. 47 | 48 | You are all set! When the deployment run completes, you will see the app at the url generated by Vercel. It can be seen in the overview page of the new project. 49 | 50 | ## Tech used 51 | 52 | - [Next.js 12.x][nextjs] 53 | - [TypeScript 4.x][typescript] 54 | - [Tailwind CSS][tailwind] 55 | 56 | ## Project structure 57 | 58 | ``` 59 | public/ 60 | src/ 61 | ├ components 62 | ├ lib 63 | │ ├ translations/ 64 | │ ├ generated-types/ 65 | │ ├ api.ts 66 | │ └ ... etc 67 | │ 68 | ├ pages/ 69 | │ ├ [locale]/ 70 | │ │ ├ articles/ 71 | │ │ │ └ [slug].tsx 72 | │ │ │ 73 | │ │ ├ [slug].tsx 74 | │ │ └ index.ts 75 | │ │ 76 | │ ├ ... 77 | │ └ index.tsx 78 | │ 79 | └ styles/ 80 | next.config.js 81 | ... 82 | ``` 83 | 84 | - **src/pages** 85 | Lists all the pages/routes on the website. See the official Next.js [documentation][pages] about pages for more information. 86 | 87 | - **src/components** 88 | It contains all React components other than Pages. The most important components here, those under `src/components/renderer`, correspond directly to the Content Types we support previewing. 89 | 90 | The `block-renderer.tsx` responsibility is to correctly render a given entry depending on its Content Type. 91 | 92 | - **src/lib** 93 | 94 | It contains any code that isn't a component or a Page, notably the fetching and translation logic and Content Types definitions (see below). 95 | 96 | ## Generating Content Types 97 | 98 | We use [cf-content-types-generator][cf-content-types-generator] to keep the Content Types definitions in `src/lib/generated-types` in sync with the space we use. 99 | 100 | ```shell 101 | # Credentials to be used by cf-content-types-generator (see package.json) 102 | export CF_SPACE_ID= 103 | export CF_CMA_TOKEN= 104 | 105 | # Generate 106 | yarn generate-types 107 | ``` 108 | 109 | ## Reach out to us 110 | 111 | ### You have questions about how to use this repo? 112 | 113 | - Reach out to our community forum: [![Contentful Community Forum](https://img.shields.io/badge/-Join%20Community%20Forum-3AB2E6.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MiA1OSI+CiAgPHBhdGggZmlsbD0iI0Y4RTQxOCIgZD0iTTE4IDQxYTE2IDE2IDAgMCAxIDAtMjMgNiA2IDAgMCAwLTktOSAyOSAyOSAwIDAgMCAwIDQxIDYgNiAwIDEgMCA5LTkiIG1hc2s9InVybCgjYikiLz4KICA8cGF0aCBmaWxsPSIjNTZBRUQyIiBkPSJNMTggMThhMTYgMTYgMCAwIDEgMjMgMCA2IDYgMCAxIDAgOS05QTI5IDI5IDAgMCAwIDkgOWE2IDYgMCAwIDAgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0UwNTM0RSIgZD0iTTQxIDQxYTE2IDE2IDAgMCAxLTIzIDAgNiA2IDAgMSAwLTkgOSAyOSAyOSAwIDAgMCA0MSAwIDYgNiAwIDAgMC05LTkiLz4KICA8cGF0aCBmaWxsPSIjMUQ3OEE0IiBkPSJNMTggMThhNiA2IDAgMSAxLTktOSA2IDYgMCAwIDEgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0JFNDMzQiIgZD0iTTE4IDUwYTYgNiAwIDEgMS05LTkgNiA2IDAgMCAxIDkgOSIvPgo8L3N2Zz4K&maxAge=31557600)][contentful-community] 114 | - Jump into our community slack channel: [![Contentful Community Slack](https://img.shields.io/badge/-Join%20Community%20Slack-2AB27B.svg?logo=slack&maxAge=31557600)][contentful-slack] 115 | 116 | ### You found a bug or want to improve this repo? 117 | 118 | - File an issue here on GitHub: [![File an issue](https://img.shields.io/badge/-Create%20Issue-6cc644.svg?logo=github&maxAge=31557600)][new-issue]. Make sure to remove any credential from your code before sharing it. 119 | 120 | ### You need to share confidential information or have other questions? 121 | 122 | - File a support ticket at our Contentful Customer Support: [![File support ticket](https://img.shields.io/badge/-Submit%20Support%20Ticket-3AB2E6.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MiA1OSI+CiAgPHBhdGggZmlsbD0iI0Y4RTQxOCIgZD0iTTE4IDQxYTE2IDE2IDAgMCAxIDAtMjMgNiA2IDAgMCAwLTktOSAyOSAyOSAwIDAgMCAwIDQxIDYgNiAwIDEgMCA5LTkiIG1hc2s9InVybCgjYikiLz4KICA8cGF0aCBmaWxsPSIjNTZBRUQyIiBkPSJNMTggMThhMTYgMTYgMCAwIDEgMjMgMCA2IDYgMCAxIDAgOS05QTI5IDI5IDAgMCAwIDkgOWE2IDYgMCAwIDAgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0UwNTM0RSIgZD0iTTQxIDQxYTE2IDE2IDAgMCAxLTIzIDAgNiA2IDAgMSAwLTkgOSAyOSAyOSAwIDAgMCA0MSAwIDYgNiAwIDAgMC05LTkiLz4KICA8cGF0aCBmaWxsPSIjMUQ3OEE0IiBkPSJNMTggMThhNiA2IDAgMSAxLTktOSA2IDYgMCAwIDEgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0JFNDMzQiIgZD0iTTE4IDUwYTYgNiAwIDEgMS05LTkgNiA2IDAgMCAxIDkgOSIvPgo8L3N2Zz4K&maxAge=31557600)][contentful-support] 123 | 124 | ## License 125 | 126 | This project is licensed under the MIT license. 127 | 128 | [nextjs]: https://nextjs.org/docs/getting-started 129 | [cf-content-types-generator]: https://github.com/contentful-labs/cf-content-types-generator 130 | [tailwind]: https://tailwindcss.com/ 131 | [typescript]: https://www.typescriptlang.org/ 132 | [pages]: https://nextjs.org/docs/basic-features/pages 133 | [contentful-community]: https://www.contentfulcommunity.com/ 134 | [contentful-support]: https://contentful.com/support 135 | [contentful-slack]: https://www.contentful.com/slack/ 136 | [new-issue]: https://github.com/contentful/compose-starter-helpcenter-nextjs/issues/new 137 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: System 3 | metadata: 4 | name: compose-starter-helpcenter-nextjs 5 | description: | 6 | A sample website frontend for Compose with Next.js 7 | annotations: 8 | circleci.com/project-slug: github/contentful/compose-starter-helpcenter-nextjs 9 | github.com/project-slug: contentful/compose-starter-helpcenter-nextjs 10 | backstage.io/source-location: url:https://github.com/contentful/compose-starter-helpcenter-nextjs/ 11 | spec: 12 | type: library 13 | lifecycle: production 14 | owner: group:team-tolkien 15 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | async redirects() { 5 | return [ 6 | { 7 | source: '//', 8 | destination: '/', 9 | permanent: true, 10 | }, 11 | ]; 12 | }, 13 | env: { 14 | CF_SPACE_ID: process.env.CF_SPACE_ID, 15 | CF_DELIVERY_ACCESS_TOKEN: process.env.CF_DELIVERY_ACCESS_TOKEN, 16 | CF_PREVIEW_ACCESS_TOKEN: process.env.CF_PREVIEW_ACCESS_TOKEN, 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cms-contentful", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "dev": "next", 6 | "build": "next build", 7 | "start": "next start", 8 | "export": "next export", 9 | "lint": "eslint . --ext '.ts,.tsx,.js,.jsx'", 10 | "prettier": "prettier --write 'src/**/*.{jsx,js,ts,tsx}'", 11 | "prettier:check": "prettier --check 'src/**/*.{jsx,js,ts,tsx}'", 12 | "tsc": "tsc", 13 | "generate-types": "cf-content-types-generator -s $CF_SPACE_ID -t $CF_CMA_TOKEN -o src/lib/generated-types", 14 | "ts": "node -r esm -r ts-node/register/transpile-only ", 15 | "prepare": "husky install" 16 | }, 17 | "dependencies": { 18 | "@contentful/rich-text-plain-text-renderer": "^15.12.0", 19 | "@contentful/rich-text-react-renderer": "^15.12.0", 20 | "@contentful/rich-text-types": "^15.12.0", 21 | "@tailwindcss/typography": "^0.5.2", 22 | "classnames": "^2.2.6", 23 | "contentful": "^9.1.18", 24 | "date-fns": "^2.28.0", 25 | "fast-safe-stringify": "^2.1.1", 26 | "lodash": "^4.17.21", 27 | "next": "^12.1.2", 28 | "react": "^18.0.0", 29 | "react-dom": "^18.0.0", 30 | "react-player": "^2.10.0" 31 | }, 32 | "devDependencies": { 33 | "@types/classnames": "^2.3.1", 34 | "@types/lodash": "^4.14.181", 35 | "@types/node": "^17.0.23", 36 | "@types/react": "^17.0.43", 37 | "@typescript-eslint/eslint-plugin": "^5.17.0", 38 | "@typescript-eslint/parser": "^5.17.0", 39 | "cf-content-types-generator": "^2.0.1", 40 | "dotenv": "16.0.0", 41 | "eslint": "^8.12.0", 42 | "eslint-config-prettier": "^8.5.0", 43 | "eslint-import-resolver-typescript": "^2.7.0", 44 | "eslint-plugin-import": "^2.25.4", 45 | "eslint-plugin-jest": "^26.1.3", 46 | "eslint-plugin-react": "^7.29.4", 47 | "eslint-plugin-react-hooks": "^4.4.0", 48 | "esm": "^3.2.25", 49 | "husky": "^7.0.0", 50 | "lint-staged": "^12.3.7", 51 | "postcss-preset-env": "^7.4.3", 52 | "prettier": "^2.6.1", 53 | "tailwindcss": "^3.0.23", 54 | "ts-node": "^10.7.0", 55 | "typescript": "^4.6.3" 56 | }, 57 | "lint-staged": { 58 | "*.{js,jsx,ts,tsx}": [ 59 | "prettier --write", 60 | "eslint" 61 | ], 62 | "*.md": [ 63 | "prettier --write" 64 | ], 65 | "*.{ts,tsx,js,jsx}": "eslint --cache --fix", 66 | "*.{js,css,md}": "prettier --write" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['tailwindcss', 'postcss-preset-env'], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/compose-starter-helpcenter-nextjs/4390d654a4317fe0e09cc53c3474416a2c90a4c0/public/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/compose-starter-helpcenter-nextjs/4390d654a4317fe0e09cc53c3474416a2c90a4c0/public/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/compose-starter-helpcenter-nextjs/4390d654a4317fe0e09cc53c3474416a2c90a4c0/public/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/compose-starter-helpcenter-nextjs/4390d654a4317fe0e09cc53c3474416a2c90a4c0/public/favicon/favicon.png -------------------------------------------------------------------------------- /src/components/RelatedPages.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { TypePage } from 'lib/types'; 4 | import { BlockRenderer } from 'components/renderer/block-renderer'; 5 | 6 | type RelatedPagesProps = { 7 | pages: Array; 8 | }; 9 | 10 | export function RelatedPages({ pages }: RelatedPagesProps) { 11 | if (pages.length === 0) { 12 | return null; 13 | } 14 | 15 | return ( 16 | <> 17 |
18 |
19 |
20 |
21 |
22 |
23 |

See also

24 |
25 | 26 |
27 | {pages.map((page, index) => ( 28 | 29 | ))} 30 |
31 |
32 |
33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/components/block-title.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function BlockTitle({ title }) { 4 | return

{title}

; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TypePage } from 'lib/types'; 3 | import NextLink from 'next/link'; 4 | import { useNavigation } from 'lib/useNavigation'; 5 | 6 | type LinkProps = { 7 | // one of them needs to be provided, RequireAtLeastOne from type-fest does not work properly 8 | page?: TypePage; 9 | path?: string; 10 | href?: string; 11 | 12 | children: React.ReactNode; 13 | }; 14 | 15 | export const Link = ({ page, path, href, children }: LinkProps) => { 16 | const { linkTo, linkToPath } = useNavigation(); 17 | const props = path ? linkToPath(path) : page ? linkTo(page) : { href }; 18 | 19 | return {children}; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export const Logo = () => ( 4 | 5 | 9 | 13 | 17 | 18 | 19 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /src/components/mobile-navigation.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { TypePage } from 'lib/types'; 4 | import { useNavigation } from 'lib/useNavigation'; 5 | import { Link } from 'components/link'; 6 | 7 | interface DropdownTriggerProps { 8 | title: string; 9 | onClick: React.MouseEventHandler; 10 | } 11 | 12 | function DropdownTrigger({ onClick, title }: DropdownTriggerProps) { 13 | return ( 14 | 27 | ); 28 | } 29 | 30 | type MobileNavigationProps = { 31 | pages: Array; 32 | }; 33 | 34 | export function MobileNavigation({ pages }: MobileNavigationProps) { 35 | const { isActive } = useNavigation(); 36 | const [isOpen, setIsOpen] = useState(false); 37 | 38 | const links = pages.map((page) => ({ 39 | page, 40 | title: page.fields.title, 41 | isActive: isActive(page), 42 | })); 43 | const activeLink = links.find(({ isActive }) => isActive === true); 44 | 45 | return ( 46 |
47 | setIsOpen(!isOpen)} title={activeLink?.title} /> 48 | {isOpen && ( 49 | 70 | )} 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/page-head.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from 'next/head'; 3 | 4 | import { TypePage } from 'lib/types'; 5 | 6 | type PageHeadProps = { 7 | page: TypePage; 8 | }; 9 | 10 | export const PageHead = ({ page }: PageHeadProps) => { 11 | const seo = page.fields.seo.fields; 12 | const { description = '', keywords = [], title = page.fields.title } = seo; 13 | const robots = [ 14 | seo.no_index === true ? 'noindex' : undefined, 15 | seo.no_follow === true ? 'nofollow' : undefined, 16 | ].filter((x) => x !== undefined); 17 | 18 | return ( 19 | 20 | {title} 21 | {robots.length > 0 && } 22 | {description.trim() !== '' && ( 23 | 24 | )} 25 | {keywords.length > 0 && } 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/page-link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import { TypePage } from 'lib/types'; 5 | import { useNavigation } from 'lib/useNavigation'; 6 | import { Link } from 'components/link'; 7 | 8 | export interface PageLinkProps { 9 | page: TypePage; 10 | } 11 | 12 | export const PageLink = ({ page }: PageLinkProps) => { 13 | const { isActive } = useNavigation(); 14 | 15 | const isActivePage = isActive(page); 16 | 17 | const linkClass = cn( 18 | 'block pl-4 align-middle text-gray-700 no-underline hover:text-blue-500 border-l-4 border-transparent', 19 | { 20 | 'lg:border-blue-500 lg:hover:border-blue-500': isActivePage, 21 | 'lg:hover:border-gray-500': !isActivePage, 22 | } 23 | ); 24 | 25 | const textClass = cn('pb-1 md:pb-0 text-sm font-medium', { 26 | 'text-blue-600 font-medium': isActivePage, 27 | }); 28 | 29 | return ( 30 |
  • 31 | 32 | 33 | {page.fields.title} 34 | 35 | 36 |
  • 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/preview-banner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | 4 | import { disablePreview } from 'lib/preview'; 5 | import { useNavigation } from 'lib/useNavigation'; 6 | 7 | export const PreviewBanner = () => { 8 | const { currentPath, isPreview, route } = useNavigation(); 9 | 10 | if (!isPreview) { 11 | return null; 12 | } 13 | 14 | const exitURL = disablePreview(currentPath); 15 | 16 | return ( 17 |
    18 |
    22 | 23 | {/* https://heroicons.dev/?search=info */} 24 | 29 | 34 | 35 | Preview mode is turned on. This enables viewing 36 | unpublished changes. 37 | 38 | 39 | Turn off 40 | 41 |
    42 |
    43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/renderer/block-renderer.tsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | 4 | import { Hero } from './hero'; 5 | import { Text } from './text'; 6 | import { Image } from './image'; 7 | import { Video } from './video'; 8 | import { Section } from './section'; 9 | import { HelpCenterArticle } from './help-center-article'; 10 | import { PageContentTypes, ComponentContentTypes } from '../../lib/constants'; 11 | 12 | type BlockRendererProps = { 13 | block: any; 14 | }; 15 | 16 | const BlockRenderer = ({ block }: BlockRendererProps) => { 17 | if (Array.isArray(block)) { 18 | return ( 19 | <> 20 | {block.map((b) => ( 21 | 22 | ))} 23 | 24 | ); 25 | } 26 | 27 | const contentTypeId = _.get(block, 'sys.contentType.sys.id'); 28 | const Component = ContentTypeMap[contentTypeId]; 29 | 30 | if (!Component) { 31 | console.warn(`${contentTypeId} can not be handled`); 32 | return null; 33 | } 34 | 35 | const { id } = block.sys; 36 | 37 | const componentProps = { 38 | ...block, 39 | parent: block.parent, 40 | }; 41 | 42 | return ; 43 | }; 44 | 45 | const ContentTypeMap = { 46 | [ComponentContentTypes.Hero]: Hero, 47 | [ComponentContentTypes.Section]: Section, 48 | [PageContentTypes.HelpDeskArticle]: HelpCenterArticle, 49 | [ComponentContentTypes.Text]: Text, 50 | [ComponentContentTypes.Image]: Image, 51 | [ComponentContentTypes.Video]: Video, 52 | }; 53 | 54 | export { BlockRenderer }; 55 | -------------------------------------------------------------------------------- /src/components/renderer/help-center-article.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import React from 'react'; 3 | 4 | import { getSummary } from 'lib/rich-text'; 5 | import { TypePage, TypePage_help_center_article } from 'lib/types'; 6 | import { Link } from 'components/link'; 7 | 8 | type HelpCenterArticleProps = TypePage_help_center_article & { 9 | parent: TypePage; 10 | }; 11 | 12 | export const HelpCenterArticle = (props) => { 13 | const { fields }: HelpCenterArticleProps = props; 14 | const summary = getSummary(fields.body); 15 | 16 | return ( 17 | // A modified version of: 18 | // https://github.com/mertJF/tailblocks/blob/master/src/blocks/blog/light/a.js 19 | 20 | 21 |
    22 |
    23 |

    24 | ARTICLE 25 |

    26 |

    {fields.title}

    27 |

    {summary}

    28 | 29 |
    30 | Learn More 31 | 40 | 41 | 42 | 43 |
    44 |
    45 |
    46 |
    47 | 48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/renderer/hero.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { Link } from 'components/link'; 3 | 4 | import { TypeComponent_hero } from 'lib/types'; 5 | import { isRichText, renderRichText } from 'lib/rich-text'; 6 | import { ComponentProps } from 'react'; 7 | 8 | export const Hero = ({ fields }: TypeComponent_hero) => { 9 | const { title, text, ctaText, ctaLink, image } = fields; 10 | const textComp = isRichText(text) ? renderRichText(text) : text; 11 | const linkProps: Omit, 'children'> = ctaLink 12 | ? { page: ctaLink } 13 | : { href: '#' }; 14 | 15 | return ( 16 |
    17 |
    18 |
    19 |

    {title}

    20 |
    {textComp}
    21 | 22 | 23 | {ctaText} 24 | 25 | 26 |
    27 |
    28 | 29 |
    30 |
    31 |
    32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/renderer/image.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import React from 'react'; 3 | 4 | import { TypeComponent_image } from 'lib/types'; 5 | 6 | const styles = { 7 | image: { 8 | margin: 0, 9 | }, 10 | }; 11 | 12 | export function Image({ fields }: Omit) { 13 | const { title, image } = fields; 14 | 15 | return ( 16 |
    17 | 18 | {title} 19 |
    20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/renderer/section.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import React from 'react'; 3 | 4 | import { BlockRenderer } from 'components/renderer/block-renderer'; 5 | import { TypeComponent_section } from 'lib/types'; 6 | 7 | const Column = ({ column }: { column: unknown }) => { 8 | return ; 9 | }; 10 | 11 | export function Section(section: TypeComponent_section) { 12 | const { columns } = section.fields; 13 | 14 | if (!columns) { 15 | return null; 16 | } 17 | 18 | return ( 19 |
    20 |
    21 |
    22 | {columns.map((column, index) => ( 23 | 24 | ))} 25 |
    26 |
    27 |
    28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/renderer/text.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import React from 'react'; 3 | 4 | import { BlockTitle } from 'components/block-title'; 5 | import { renderRichText } from 'lib/rich-text'; 6 | import { TypeComponent_text } from 'lib/types'; 7 | 8 | export function Text({ fields }: TypeComponent_text) { 9 | const { title, text } = fields; 10 | 11 | return ( 12 | <> 13 | {title ? : null} 14 | {renderRichText(text as any)} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/renderer/video.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import React, { useState, useEffect } from 'react'; 3 | import ReactPlayer from 'react-player'; 4 | 5 | import { TypeComponent_video } from 'lib/types'; 6 | 7 | const styles = { 8 | container: { 9 | paddingBottom: '56.25%', // 16/9 ratio 10 | }, 11 | }; 12 | 13 | export function Video({ fields }: Omit) { 14 | const [isSSR, setIsSSR] = useState(true); 15 | const { title, youtubeVideoId } = fields; 16 | 17 | // fix for issue with React v18 and react player 18 | // Hydration failed because the initial UI does not match what was rendered on the server. 19 | // https://github.com/cookpete/react-player/issues/1428 20 | useEffect(() => { 21 | setIsSSR(false); 22 | }, []); 23 | 24 | if (!youtubeVideoId) { 25 | return null; 26 | } 27 | 28 | return ( 29 |
    30 |
    31 | {!isSSR && ( 32 | 39 | )} 40 |
    41 | {title} 42 |
    43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/top-navigation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'components/link'; 3 | 4 | import { Logo } from 'components/logo'; 5 | import { SITE_NAME } from 'lib/constants'; 6 | 7 | export function TopNavigation() { 8 | return ( 9 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from 'contentful'; 2 | 3 | import { parsePage } from './pageParsers'; 4 | import { Locale } from './translations'; 5 | 6 | const client = createClient({ 7 | space: process.env.CF_SPACE_ID, 8 | accessToken: process.env.CF_DELIVERY_ACCESS_TOKEN, 9 | }); 10 | 11 | const previewClient = createClient({ 12 | space: process.env.CF_SPACE_ID, 13 | accessToken: process.env.CF_PREVIEW_ACCESS_TOKEN, 14 | host: 'preview.contentful.com', 15 | }); 16 | 17 | const getClient = (preview: boolean) => (preview ? previewClient : client); 18 | 19 | type GetPageParams = { 20 | slug: string; 21 | locale: Locale; 22 | pageContentType: string; 23 | preview?: boolean; 24 | }; 25 | 26 | const getPageQuery = (params: GetPageParams) => ({ 27 | limit: 1, 28 | include: 10, 29 | locale: params.locale, 30 | 'fields.slug': params.slug, 31 | content_type: params.pageContentType, 32 | }); 33 | 34 | export async function getPage(params: GetPageParams) { 35 | const query = getPageQuery(params); 36 | const { items } = await getClient(params.preview).getEntries(query); 37 | const page = items[0]; 38 | 39 | return page ? parsePage(page) : null; 40 | } 41 | 42 | type GetPagesOfTypeParams = { 43 | locale: Locale; 44 | pageContentType: string; 45 | preview?: boolean; 46 | }; 47 | 48 | export async function getPagesOfType(params: GetPagesOfTypeParams) { 49 | const { pageContentType, preview, locale } = params; 50 | const client = getClient(preview); 51 | 52 | const { items: pages } = await client.getEntries({ 53 | limit: 100, 54 | locale, 55 | content_type: pageContentType, 56 | }); 57 | 58 | return pages ? pages.map((page) => parsePage(page)) : []; 59 | } 60 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const SITE_NAME = 'Help Center'; 2 | 3 | export const PageContentType = 'page'; 4 | 5 | export const ComponentContentTypes = { 6 | Section: 'component_section', 7 | Hero: 'component_hero', 8 | Text: 'component_text', 9 | Image: 'component_image', 10 | Video: 'component_video', 11 | }; 12 | 13 | export const PageContentTypes = { 14 | HelpDeskArticle: 'page_help_center_article', 15 | LandingPage: 'page_landing', 16 | }; 17 | 18 | export const fallbackImage = { 19 | title: 'Thumbnail placeholder', 20 | url: 'https://dummyimage.com/720x400', 21 | }; 22 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypeComponent_hero.ts: -------------------------------------------------------------------------------- 1 | import * as CFRichTextTypes from '@contentful/rich-text-types'; 2 | import * as Contentful from 'contentful'; 3 | import { TypePage_help_center_articleFields } from './TypePage_help_center_article'; 4 | import { TypePage_landingFields } from './TypePage_landing'; 5 | 6 | export interface TypeComponent_heroFields { 7 | name: Contentful.EntryFields.Symbol; 8 | title: Contentful.EntryFields.Symbol; 9 | text?: CFRichTextTypes.Block | CFRichTextTypes.Inline; 10 | image: Contentful.Asset; 11 | ctaText: Contentful.EntryFields.Symbol; 12 | ctaLink?: Contentful.Entry; 13 | } 14 | 15 | export type TypeComponent_hero = Contentful.Entry; 16 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypeComponent_image.ts: -------------------------------------------------------------------------------- 1 | import * as Contentful from 'contentful'; 2 | 3 | export interface TypeComponent_imageFields { 4 | name: Contentful.EntryFields.Symbol; 5 | title?: Contentful.EntryFields.Symbol; 6 | image: Contentful.Asset; 7 | } 8 | 9 | export type TypeComponent_image = Contentful.Entry; 10 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypeComponent_section.ts: -------------------------------------------------------------------------------- 1 | import * as Contentful from 'contentful'; 2 | 3 | export interface TypeComponent_sectionFields { 4 | name: Contentful.EntryFields.Symbol; 5 | columns?: Contentful.Entry>[]; 6 | } 7 | 8 | export type TypeComponent_section = Contentful.Entry; 9 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypeComponent_text.ts: -------------------------------------------------------------------------------- 1 | import * as CFRichTextTypes from '@contentful/rich-text-types'; 2 | import * as Contentful from 'contentful'; 3 | 4 | export interface TypeComponent_textFields { 5 | title?: Contentful.EntryFields.Symbol; 6 | text: CFRichTextTypes.Block | CFRichTextTypes.Inline; 7 | } 8 | 9 | export type TypeComponent_text = Contentful.Entry; 10 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypeComponent_video.ts: -------------------------------------------------------------------------------- 1 | import * as Contentful from 'contentful'; 2 | 3 | export interface TypeComponent_videoFields { 4 | title?: Contentful.EntryFields.Symbol; 5 | youtubeVideoId: Contentful.EntryFields.Symbol; 6 | } 7 | 8 | export type TypeComponent_video = Contentful.Entry; 9 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypePage_help_center_article.ts: -------------------------------------------------------------------------------- 1 | import * as Contentful from 'contentful'; 2 | import { TypeComponent_imageFields } from './TypeComponent_image'; 3 | import { TypeComponent_textFields } from './TypeComponent_text'; 4 | import { TypeComponent_videoFields } from './TypeComponent_video'; 5 | import { TypeSeoFields } from './TypeSeo'; 6 | 7 | export interface TypePage_help_center_articleFields { 8 | name: Contentful.EntryFields.Symbol; 9 | title: Contentful.EntryFields.Symbol; 10 | slug: Contentful.EntryFields.Symbol; 11 | body: Contentful.Entry< 12 | TypeComponent_imageFields | TypeComponent_textFields | TypeComponent_videoFields 13 | >[]; 14 | relatedPages?: Contentful.Entry[]; 15 | seo?: Contentful.Entry; 16 | } 17 | 18 | export type TypePage_help_center_article = Contentful.Entry; 19 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypePage_landing.ts: -------------------------------------------------------------------------------- 1 | import * as Contentful from 'contentful'; 2 | import { TypeComponent_heroFields } from './TypeComponent_hero'; 3 | import { TypeComponent_sectionFields } from './TypeComponent_section'; 4 | import { TypeSeoFields } from './TypeSeo'; 5 | 6 | export interface TypePage_landingFields { 7 | name: Contentful.EntryFields.Symbol; 8 | title: Contentful.EntryFields.Symbol; 9 | slug: Contentful.EntryFields.Symbol; 10 | hero: Contentful.Entry; 11 | sections: Contentful.Entry[]; 12 | seo?: Contentful.Entry; 13 | } 14 | 15 | export type TypePage_landing = Contentful.Entry; 16 | -------------------------------------------------------------------------------- /src/lib/generated-types/TypeSeo.ts: -------------------------------------------------------------------------------- 1 | import * as Contentful from 'contentful'; 2 | 3 | export interface TypeSeoFields { 4 | name: Contentful.EntryFields.Symbol; 5 | title?: Contentful.EntryFields.Symbol; 6 | description?: Contentful.EntryFields.Symbol; 7 | keywords?: Contentful.EntryFields.Symbol[]; 8 | no_index?: Contentful.EntryFields.Boolean; 9 | no_follow?: Contentful.EntryFields.Boolean; 10 | } 11 | 12 | export type TypeSeo = Contentful.Entry; 13 | -------------------------------------------------------------------------------- /src/lib/generated-types/index.ts: -------------------------------------------------------------------------------- 1 | export type { TypeComponent_hero, TypeComponent_heroFields } from './TypeComponent_hero'; 2 | export type { TypeComponent_image, TypeComponent_imageFields } from './TypeComponent_image'; 3 | export type { TypeComponent_section, TypeComponent_sectionFields } from './TypeComponent_section'; 4 | export type { TypeComponent_text, TypeComponent_textFields } from './TypeComponent_text'; 5 | export type { TypeComponent_video, TypeComponent_videoFields } from './TypeComponent_video'; 6 | export type { 7 | TypePage_help_center_article, 8 | TypePage_help_center_articleFields, 9 | } from './TypePage_help_center_article'; 10 | export type { TypePage_landing, TypePage_landingFields } from './TypePage_landing'; 11 | export type { TypeSeo, TypeSeoFields } from './TypeSeo'; 12 | -------------------------------------------------------------------------------- /src/lib/pageParsers.ts: -------------------------------------------------------------------------------- 1 | import stringify from 'fast-safe-stringify'; 2 | 3 | import { TypePage } from './types'; 4 | 5 | export const parsePage = (page: unknown): TypePage => { 6 | // Kill circular references 7 | return JSON.parse(stringify(page)) as TypePage; 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/preview.ts: -------------------------------------------------------------------------------- 1 | const ENABLED = '1'; 2 | 3 | export const disablePreview = (url: string) => { 4 | const pattern = /preview=[^\\&]+&?/; 5 | return url.replace(pattern, ''); 6 | }; 7 | 8 | export const isPreviewEnabled = (query: Record) => { 9 | const param = String(query?.preview).toLowerCase(); 10 | return param === ENABLED; 11 | }; 12 | 13 | export const withPreviewParam = (url: string, isPreview: boolean) => { 14 | const query = isPreview ? `?preview=${ENABLED}` : ''; 15 | return url + query; 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/rich-text/embedded-asset.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import noop from 'lodash/noop'; 3 | import { Image } from 'components/renderer/image'; 4 | import { Video } from 'components/renderer/video'; 5 | import { NodeRenderer } from '@contentful/rich-text-react-renderer'; 6 | 7 | export const EmbeddedAsset = (({ 8 | data: { 9 | target: { sys, fields }, 10 | }, 11 | }) => { 12 | const isVideo = fields.file.contentType.includes('video'); 13 | if (isVideo) { 14 | return ( 15 |