├── src ├── react-app-env.d.ts ├── setupTests.ts ├── locations │ ├── Home.spec.tsx │ ├── Page.spec.tsx │ ├── Dialog.spec.tsx │ ├── Field.spec.tsx │ ├── Sidebar.spec.tsx │ ├── Home.tsx │ ├── Page.tsx │ ├── Dialog.tsx │ ├── EntryEditor.spec.tsx │ ├── EntryEditor.tsx │ ├── Sidebar.tsx │ ├── ConfigScreen.spec.tsx │ ├── Field.tsx │ └── ConfigScreen.tsx ├── index.tsx ├── App.tsx └── components │ └── LocalhostWarning.tsx ├── test └── mocks │ ├── mockCma.ts │ ├── index.ts │ └── mockSdk.ts ├── tsconfig.json ├── .gitignore ├── public └── index.html ├── package.json └── README.md /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /test/mocks/mockCma.ts: -------------------------------------------------------------------------------- 1 | const mockCma: any = {}; 2 | 3 | export { mockCma }; 4 | -------------------------------------------------------------------------------- /test/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export { mockCma } from './mockCma'; 2 | export { mockSdk } from './mockSdk'; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/create-react-app/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /test/mocks/mockSdk.ts: -------------------------------------------------------------------------------- 1 | const mockSdk: any = { 2 | app: { 3 | onConfigure: jest.fn(), 4 | getParameters: jest.fn().mockReturnValueOnce({}), 5 | setReady: jest.fn(), 6 | getCurrentState: jest.fn(), 7 | }, 8 | ids: { 9 | app: 'test-app' 10 | } 11 | }; 12 | 13 | export { mockSdk }; 14 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | import { configure } from '@testing-library/react'; 7 | 8 | configure({ 9 | testIdAttribute: 'data-test-id', 10 | }); 11 | -------------------------------------------------------------------------------- /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env -------------------------------------------------------------------------------- /src/locations/Home.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Home from './Home'; 3 | import { render } from '@testing-library/react'; 4 | import { mockCma, mockSdk } from '../../test/mocks'; 5 | 6 | jest.mock('@contentful/react-apps-toolkit', () => ({ 7 | useSDK: () => mockSdk, 8 | useCMA: () => mockCma, 9 | })); 10 | 11 | describe('Home component', () => { 12 | it('Component text exists', () => { 13 | const { getByText } = render(); 14 | 15 | expect(getByText('Hello Home Component (AppId: test-app)')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/locations/Page.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Page from './Page'; 3 | import { render } from '@testing-library/react'; 4 | import { mockCma, mockSdk } from '../../test/mocks'; 5 | 6 | jest.mock('@contentful/react-apps-toolkit', () => ({ 7 | useSDK: () => mockSdk, 8 | useCMA: () => mockCma, 9 | })); 10 | 11 | describe('Page component', () => { 12 | it('Component text exists', () => { 13 | const { getByText } = render(); 14 | 15 | expect(getByText('Hello Page Component (AppId: test-app)')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/locations/Dialog.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dialog from './Dialog'; 3 | import { render } from '@testing-library/react'; 4 | import { mockCma, mockSdk } from '../../test/mocks'; 5 | 6 | jest.mock('@contentful/react-apps-toolkit', () => ({ 7 | useSDK: () => mockSdk, 8 | useCMA: () => mockCma, 9 | })); 10 | 11 | describe('Dialog component', () => { 12 | it('Component text exists', () => { 13 | const { getByText } = render(); 14 | 15 | expect(getByText('Hello Dialog Component (AppId: test-app)')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/locations/Field.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Field from './Field'; 3 | import { render } from '@testing-library/react'; 4 | import { mockCma, mockSdk } from '../../test/mocks'; 5 | 6 | jest.mock('@contentful/react-apps-toolkit', () => ({ 7 | useSDK: () => mockSdk, 8 | useCMA: () => mockCma, 9 | })); 10 | 11 | describe('Field component', () => { 12 | it('Component text exists', () => { 13 | const { getByText } = render(); 14 | 15 | expect(getByText('Hello Entry Field Component (AppId: test-app)')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/locations/Sidebar.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Sidebar from './Sidebar'; 3 | import { render } from '@testing-library/react'; 4 | import { mockCma, mockSdk } from '../../test/mocks'; 5 | 6 | jest.mock('@contentful/react-apps-toolkit', () => ({ 7 | useSDK: () => mockSdk, 8 | useCMA: () => mockCma, 9 | })); 10 | 11 | describe('Sidebar component', () => { 12 | it('Component text exists', () => { 13 | const { getByText } = render(); 14 | 15 | expect(getByText('Hello Sidebar Component (AppId: test-app)')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/locations/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/f36-components'; 3 | import { HomeExtensionSDK } from '@contentful/app-sdk'; 4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit'; 5 | 6 | const Home = () => { 7 | const sdk = useSDK(); 8 | /* 9 | To use the cma, inject it as follows. 10 | If it is not needed, you can remove the next line. 11 | */ 12 | // const cma = useCMA(); 13 | 14 | return Hello Home Component (AppId: {sdk.ids.app}); 15 | }; 16 | 17 | export default Home; 18 | -------------------------------------------------------------------------------- /src/locations/Page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/f36-components'; 3 | import { PageExtensionSDK } from '@contentful/app-sdk'; 4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit'; 5 | 6 | const Page = () => { 7 | const sdk = useSDK(); 8 | /* 9 | To use the cma, inject it as follows. 10 | If it is not needed, you can remove the next line. 11 | */ 12 | // const cma = useCMA(); 13 | 14 | return Hello Page Component (AppId: {sdk.ids.app}); 15 | }; 16 | 17 | export default Page; 18 | -------------------------------------------------------------------------------- /src/locations/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/f36-components'; 3 | import { DialogExtensionSDK } from '@contentful/app-sdk'; 4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit'; 5 | 6 | const Dialog = () => { 7 | const sdk = useSDK(); 8 | /* 9 | To use the cma, inject it as follows. 10 | If it is not needed, you can remove the next line. 11 | */ 12 | // const cma = useCMA(); 13 | 14 | return Hello Dialog Component (AppId: {sdk.ids.app}); 15 | }; 16 | 17 | export default Dialog; 18 | -------------------------------------------------------------------------------- /src/locations/EntryEditor.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EntryEditor from './EntryEditor'; 3 | import { render } from '@testing-library/react'; 4 | import { mockCma, mockSdk } from '../../test/mocks'; 5 | 6 | jest.mock('@contentful/react-apps-toolkit', () => ({ 7 | useSDK: () => mockSdk, 8 | useCMA: () => mockCma, 9 | })); 10 | 11 | describe('Entry component', () => { 12 | it('Component text exists', () => { 13 | const { getByText } = render(); 14 | 15 | expect(getByText('Hello Entry Editor Component (AppId: test-app)')).toBeInTheDocument(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/locations/EntryEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/f36-components'; 3 | import { EditorExtensionSDK } from '@contentful/app-sdk'; 4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit'; 5 | 6 | const Entry = () => { 7 | const sdk = useSDK(); 8 | /* 9 | To use the cma, inject it as follows. 10 | If it is not needed, you can remove the next line. 11 | */ 12 | // const cma = useCMA(); 13 | 14 | return Hello Entry Editor Component (AppId: {sdk.ids.app}); 15 | }; 16 | 17 | export default Entry; 18 | -------------------------------------------------------------------------------- /src/locations/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/f36-components'; 3 | import { SidebarExtensionSDK } from '@contentful/app-sdk'; 4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit'; 5 | 6 | const Sidebar = () => { 7 | const sdk = useSDK(); 8 | /* 9 | To use the cma, inject it as follows. 10 | If it is not needed, you can remove the next line. 11 | */ 12 | // const cma = useCMA(); 13 | 14 | return Hello Sidebar Component (AppId: {sdk.ids.app}); 15 | }; 16 | 17 | export default Sidebar; 18 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import { GlobalStyles } from '@contentful/f36-components'; 5 | import { SDKProvider } from '@contentful/react-apps-toolkit'; 6 | 7 | import LocalhostWarning from './components/LocalhostWarning'; 8 | import App from './App'; 9 | 10 | const root = document.getElementById('root'); 11 | 12 | if (process.env.NODE_ENV === 'development' && window.self === window.top) { 13 | // You can remove this if block before deploying your app 14 | render(, root); 15 | } else { 16 | render( 17 | 18 | 19 | 20 | , 21 | root 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/locations/ConfigScreen.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ConfigScreen from './ConfigScreen'; 3 | import { render } from '@testing-library/react'; 4 | import { mockCma, mockSdk } from '../../test/mocks'; 5 | 6 | jest.mock('@contentful/react-apps-toolkit', () => ({ 7 | useSDK: () => mockSdk, 8 | useCMA: () => mockCma, 9 | })); 10 | 11 | describe('Config Screen component', () => { 12 | it('Component text exists', async () => { 13 | const { getByText } = render(); 14 | 15 | // simulate the user clicking the install button 16 | await mockSdk.app.onConfigure.mock.calls[0][0](); 17 | 18 | expect( 19 | getByText('Welcome to your contentful app. This is your config page.') 20 | ).toBeInTheDocument(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/locations/Field.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph } from '@contentful/f36-components'; 3 | import { FieldExtensionSDK } from '@contentful/app-sdk'; 4 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit'; 5 | 6 | const Field = () => { 7 | const sdk = useSDK(); 8 | /* 9 | To use the cma, inject it as follows. 10 | If it is not needed, you can remove the next line. 11 | */ 12 | // const cma = useCMA(); 13 | // If you only want to extend Contentful's default editing experience 14 | // reuse Contentful's editor components 15 | // -> https://www.contentful.com/developers/docs/extensibility/field-editors/ 16 | return Hello Entry Field Component (AppId: {sdk.ids.app}); 17 | }; 18 | 19 | export default Field; 20 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { locations } from '@contentful/app-sdk'; 3 | import ConfigScreen from './locations/ConfigScreen'; 4 | import Field from './locations/Field'; 5 | import EntryEditor from './locations/EntryEditor'; 6 | import Dialog from './locations/Dialog'; 7 | import Sidebar from './locations/Sidebar'; 8 | import Page from './locations/Page'; 9 | import Home from './locations/Home'; 10 | import { useSDK } from '@contentful/react-apps-toolkit'; 11 | 12 | const ComponentLocationSettings = { 13 | [locations.LOCATION_APP_CONFIG]: ConfigScreen, 14 | [locations.LOCATION_ENTRY_FIELD]: Field, 15 | [locations.LOCATION_ENTRY_EDITOR]: EntryEditor, 16 | [locations.LOCATION_DIALOG]: Dialog, 17 | [locations.LOCATION_ENTRY_SIDEBAR]: Sidebar, 18 | [locations.LOCATION_PAGE]: Page, 19 | [locations.LOCATION_HOME]: Home, 20 | }; 21 | 22 | const App = () => { 23 | const sdk = useSDK(); 24 | 25 | const Component = useMemo(() => { 26 | for (const [location, component] of Object.entries(ComponentLocationSettings)) { 27 | if (sdk.location.is(location)) { 28 | return component; 29 | } 30 | } 31 | }, [sdk.location]); 32 | 33 | return Component ? : null; 34 | }; 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /src/components/LocalhostWarning.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Paragraph, TextLink, Note, Flex } from '@contentful/f36-components'; 3 | 4 | const LocalhostWarning = () => { 5 | return ( 6 | 7 | 8 | 9 | Contentful Apps need to run inside the Contentful web app to function properly. Install 10 | the app into a space and render your app into one of the{' '} 11 | 12 | available locations 13 | 14 | . 15 | 16 |
17 | 18 | 19 | Follow{' '} 20 | 21 | our guide 22 | {' '} 23 | to get started or{' '} 24 | open Contentful{' '} 25 | to manage your app. 26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default LocalhostWarning; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground-testing", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@contentful/app-sdk": "4.13.0", 7 | "@contentful/f36-components": "4.21.8", 8 | "@contentful/f36-tokens": "4.0.1", 9 | "@contentful/react-apps-toolkit": "1.2.9", 10 | "contentful-management": "10.21.1", 11 | "emotion": "10.0.27", 12 | "react": "17.0.2", 13 | "react-dom": "17.0.2", 14 | "react-scripts": "5.0.1" 15 | }, 16 | "scripts": { 17 | "start": "cross-env BROWSER=none react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "create-app-definition": "contentful-app-scripts create-app-definition", 22 | "upload": "contentful-app-scripts upload --bundle-dir ./build", 23 | "upload-ci": "contentful-app-scripts upload --ci --bundle-dir ./build --organization-id $CONTENTFUL_ORG_ID --definition-id $CONTENTFUL_APP_DEF_ID --token $CONTENTFUL_ACCESS_TOKEN", 24 | "open-playground": "source .env && open 'https://contentful-apps-playground.netlify.app/?token='$CONTENTFUL_ACCESS_TOKEN'&orgId='$CONTENTFUL_ORG_ID'&appId='$CONTENTFUL_APP_DEF_ID" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@contentful/app-scripts": "1.2.1", 43 | "@testing-library/jest-dom": "5.16.5", 44 | "@testing-library/react": "12.1.5", 45 | "@tsconfig/create-react-app": "1.0.3", 46 | "@types/jest": "29.2.3", 47 | "@types/node": "18.11.9", 48 | "@types/react": "18.0.9", 49 | "@types/react-dom": "18.0.3", 50 | "cross-env": "7.0.3", 51 | "typescript": "4.9.3" 52 | }, 53 | "homepage": "." 54 | } 55 | -------------------------------------------------------------------------------- /src/locations/ConfigScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState, useEffect } from 'react'; 2 | import { AppExtensionSDK } from '@contentful/app-sdk'; 3 | import { Heading, Form, Paragraph, Flex } from '@contentful/f36-components'; 4 | import { css } from 'emotion'; 5 | import { /* useCMA, */ useSDK } from '@contentful/react-apps-toolkit'; 6 | 7 | export interface AppInstallationParameters {} 8 | 9 | const ConfigScreen = () => { 10 | const [parameters, setParameters] = useState({}); 11 | const sdk = useSDK(); 12 | /* 13 | To use the cma, inject it as follows. 14 | If it is not needed, you can remove the next line. 15 | */ 16 | // const cma = useCMA(); 17 | 18 | const onConfigure = useCallback(async () => { 19 | // This method will be called when a user clicks on "Install" 20 | // or "Save" in the configuration screen. 21 | // for more details see https://www.contentful.com/developers/docs/extensibility/ui-extensions/sdk-reference/#register-an-app-configuration-hook 22 | 23 | // Get current the state of EditorInterface and other entities 24 | // related to this app installation 25 | const currentState = await sdk.app.getCurrentState(); 26 | 27 | return { 28 | // Parameters to be persisted as the app configuration. 29 | parameters, 30 | // In case you don't want to submit any update to app 31 | // locations, you can just pass the currentState as is 32 | targetState: currentState, 33 | }; 34 | }, [parameters, sdk]); 35 | 36 | useEffect(() => { 37 | // `onConfigure` allows to configure a callback to be 38 | // invoked when a user attempts to install the app or update 39 | // its configuration. 40 | sdk.app.onConfigure(() => onConfigure()); 41 | }, [sdk, onConfigure]); 42 | 43 | useEffect(() => { 44 | (async () => { 45 | // Get current parameters of the app. 46 | // If the app is not installed yet, `parameters` will be `null`. 47 | const currentParameters: AppInstallationParameters | null = await sdk.app.getParameters(); 48 | 49 | if (currentParameters) { 50 | setParameters(currentParameters); 51 | } 52 | 53 | // Once preparation has finished, call `setReady` to hide 54 | // the loading screen and present the app to a user. 55 | sdk.app.setReady(); 56 | })(); 57 | }, [sdk]); 58 | 59 | return ( 60 | 61 |
62 | App Config 63 | Welcome to your contentful app. This is your config page. 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default ConfigScreen; 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create Contentful App](https://github.com/contentful/create-contentful-app). 2 | 3 | ## How to use 4 | 5 | Execute create-contentful-app with npm, npx or yarn to bootstrap the example: 6 | 7 | ```bash 8 | # npx 9 | npx create-contentful-app --typescript 10 | 11 | # npm 12 | npm init contentful-app -- --typescript 13 | 14 | # Yarn 15 | yarn create contentful-app --typescript 16 | ``` 17 | 18 | ## Available Scripts 19 | 20 | In the project directory, you can run: 21 | 22 | #### `npm start` 23 | 24 | Creates or updates your app definition in Contentful, and runs the app in development mode. 25 | Open your app to view it in the browser. 26 | 27 | The page will reload if you make edits. 28 | You will also see any lint errors in the console. 29 | 30 | #### `npm run build` 31 | 32 | Builds the app for production to the `build` folder. 33 | It correctly bundles React in production mode and optimizes the build for the best performance. 34 | 35 | The build is minified and the filenames include the hashes. 36 | Your app is ready to be deployed! 37 | 38 | #### `npm run upload` 39 | 40 | Uploads the build folder to contentful and creates a bundle that is automatically activated. 41 | The command guides you through the deployment process and asks for all required arguments. 42 | Read [here](https://www.contentful.com/developers/docs/extensibility/app-framework/create-contentful-app/#deploy-with-contentful) for more information about the deployment process. 43 | 44 | #### `npm run upload-ci` 45 | 46 | Similar to `npm run upload` it will upload your app to contentful and activate it. The only difference is 47 | that with this command all required arguments are read from the environment variables, for example when you add 48 | the upload command to your CI pipeline. 49 | 50 | For this command to work, the following environment variables must be set: 51 | 52 | - `CONTENTFUL_ORG_ID` - The ID of your organization 53 | - `CONTENTFUL_APP_DEF_ID` - The ID of the app to which to add the bundle 54 | - `CONTENTFUL_ACCESS_TOKEN` - A personal [access token](https://www.contentful.com/developers/docs/references/content-management-api/#/reference/personal-access-tokens) 55 | 56 | ## Libraries to use 57 | 58 | To make your app look and feel like Contentful use the following libraries: 59 | 60 | - [Forma 36](https://f36.contentful.com/) – Contentful's design system 61 | - [Contentful Field Editors](https://www.contentful.com/developers/docs/extensibility/field-editors/) – Contentful's field editor React components 62 | 63 | ## Using the `contentful-management` SDK 64 | 65 | In the default create contentful app output, a contentful management client is 66 | passed into each location. This can be used to interact with Contentful's 67 | management API. For example 68 | 69 | ```js 70 | // Use the client 71 | cma.locale.getMany({}).then((locales) => console.log(locales)) 72 | 73 | ``` 74 | 75 | Visit the [`contentful-management` documentation](https://www.contentful.com/developers/docs/extensibility/app-framework/sdk/#using-the-contentful-management-library) 76 | to find out more. 77 | 78 | ## Learn More 79 | 80 | [Read more](https://www.contentful.com/developers/docs/extensibility/app-framework/create-contentful-app/) and check out the video on how to use the CLI. 81 | 82 | Create Contentful App uses [Create React App](https://create-react-app.dev/). You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started) and how to further customize your app. 83 | --------------------------------------------------------------------------------