├── .babelrc
├── src
├── .babelrc
├── index.js
├── initContentful.js
└── withContentful.js
├── .gitignore
├── .npmignore
├── tools
├── jest-setup.js
├── babel-preset.js
└── build.js
├── types
├── tsconfig.json
└── index.d.ts
├── .github
└── FUNDING.yml
├── LICENSE
├── rollup.config.js
├── README.md
└── package.json
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "./tools/babel-preset" ]
3 | }
4 |
--------------------------------------------------------------------------------
/src/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ "../tools/babel-preset" ]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | coverage
3 | es
4 | node_modules
5 | umd
6 | /*.js
7 | !rollup.config.js
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | coverage
2 | docs
3 | site
4 | tools
5 | src/.babelrc
6 | **/*.test.js
7 | rollup.config.js
8 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { default as initContentful } from './initContentful';
2 | export { default as withContentful } from './withContentful';
3 |
--------------------------------------------------------------------------------
/tools/jest-setup.js:
--------------------------------------------------------------------------------
1 | import jsdom from 'jsdom';
2 | const { JSDOM } = jsdom;
3 |
4 | global.document = new JSDOM('
');
5 |
--------------------------------------------------------------------------------
/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "lib": ["es6"],
5 | "noImplicitAny": true,
6 | "noImplicitThis": true,
7 | "strictNullChecks": true,
8 | "strictFunctionTypes": true,
9 | "noEmit": true
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | // TypeScript Version: 3.0
2 |
3 | import { Component } from 'react';
4 | import { ContentfulClientInterface } from 'react-contentful';
5 |
6 | /**
7 | * initContentful
8 | */
9 |
10 | export interface initContentfulParams {
11 | initialState: any;
12 | }
13 |
14 | export function initContentful(params: initContentfulParams): ContentfulClientInterface;
15 |
16 | /**
17 | * withContentful
18 | */
19 |
20 | export interface withContentfulParams {
21 | accessToken: string;
22 | host: string;
23 | space: string;
24 | environment?: string;
25 | }
26 |
27 | export function withContentful(params: withContentfulParams): (app: any) => Component;
28 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: ryanhefner # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: ryanhefner # Replace with a single Patreon username
5 | open_collective: ryanhefner # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with a single custom sponsorship URL
13 |
--------------------------------------------------------------------------------
/src/initContentful.js:
--------------------------------------------------------------------------------
1 | import { ContentfulCache, ContentfulClient } from 'react-contentful';
2 |
3 | let client = null;
4 |
5 | function create({
6 | accessToken,
7 | cache,
8 | host,
9 | space,
10 | environment = 'master',
11 | }) {
12 | return new ContentfulClient({
13 | host,
14 | accessToken,
15 | space,
16 | environment,
17 | ssrMode: !!(process.browser) === false,
18 | cache: cache instanceof ContentfulCache
19 | ? cache
20 | : (new ContentfulCache()).restore(cache),
21 | });
22 | }
23 |
24 | export default function initContentful(initialState = {}) {
25 | if (!process.browser) {
26 | return create(initialState);
27 | }
28 |
29 | if (!client) {
30 | client = create(initialState);
31 | }
32 |
33 | return client;
34 | }
35 |
--------------------------------------------------------------------------------
/tools/babel-preset.js:
--------------------------------------------------------------------------------
1 | const BABEL_ENV = process.env.BABEL_ENV;
2 | const building = BABEL_ENV != undefined && BABEL_ENV !== 'cjs';
3 |
4 | const plugins = [
5 | '@babel/plugin-proposal-class-properties',
6 | '@babel/plugin-proposal-object-rest-spread',
7 | '@babel/plugin-transform-runtime',
8 | ];
9 |
10 | if (process.env.NODE_ENV === 'production') {
11 | plugins.push(
12 | 'babel-plugin-dev-expression',
13 | 'babel-plugin-transform-react-remove-prop-types'
14 | );
15 | }
16 |
17 | module.exports = () => {
18 | return {
19 | env: {
20 | test: {
21 | presets: [['@babel/preset-env'], '@babel/preset-react'],
22 | },
23 | },
24 | presets: [
25 | ['@babel/preset-env', {
26 | 'loose': true,
27 | 'modules': building ? false : 'commonjs'
28 | }],
29 | '@babel/preset-react'
30 | ],
31 | plugins: plugins,
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2020 Ryan Hefner (https://www.ryanhefner.com)
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 |
--------------------------------------------------------------------------------
/tools/build.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const execSync = require('child_process').execSync;
3 | const prettyBytes = require('pretty-bytes');
4 | const gzipSize = require('gzip-size');
5 |
6 | const exec = (command, extraEnv) => {
7 | execSync(command, {
8 | stdio: 'inherit',
9 | env: Object.assign({}, process.env, extraEnv),
10 | });
11 | };
12 |
13 | console.log('Building CommonJS modules ...');
14 |
15 | exec('babel src -d . --ignore src/__mocks__,__tests__,**/*.test.js', {
16 | BABEL_ENV: 'cjs',
17 | });
18 |
19 | console.log('\nBuilding ES modules ...');
20 |
21 | exec('babel src -d es --ignore src/__mocks__,__tests__,**/*.test.js', {
22 | BABEL_ENV: 'es',
23 | });
24 |
25 | // console.log('\nBuilding next-contentful.js ...');
26 |
27 | // exec('rollup -c -f umd -o umd/next-contentful.js', {
28 | // BABEL_ENV: 'umd',
29 | // NODE_ENV: 'development',
30 | // });
31 |
32 | // console.log('\nBuilding next-contentful.min.js ...');
33 |
34 | // exec('rollup -c -f umd -o umd/next-contentful.min.js', {
35 | // BABEL_ENV: 'umd',
36 | // NODE_ENV: 'production',
37 | // });
38 |
39 | // const size = gzipSize.sync(
40 | // fs.readFileSync('umd/next-contentful.min.js')
41 | // );
42 |
43 | // console.log('\ngzipped, the UMD build is %s', prettyBytes(size));
44 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import json from 'rollup-plugin-json';
4 | import resolve from 'rollup-plugin-node-resolve';
5 | import { uglify } from 'rollup-plugin-uglify';
6 | import pkg from './package.json';
7 |
8 | const config = {
9 | input: 'src/index.js',
10 | output: {
11 | name: 'next-contentful',
12 | file: './index.js',
13 | format: 'umd',
14 | globals: {
15 | 'next/head': 'Head',
16 | 'react': 'React',
17 | 'react-dom': 'ReactDOM',
18 | },
19 | banner: `/*! ${pkg.name} v${pkg.version} | (c) ${new Date().getFullYear()} Ryan Hefner | ${pkg.license} License | https://github.com/${pkg.repository} !*/`,
20 | footer: '/* follow me on Twitter! @ryanhefner */',
21 | },
22 | external: [
23 | 'react',
24 | 'react-dom',
25 | ],
26 | plugins: [
27 | babel({
28 | exclude: 'node_modules/**',
29 | externalHelpers: process.env.BABEL_ENV === 'umd',
30 | runtimeHelpers: true,
31 | }),
32 | resolve(),
33 | commonjs({
34 | include: /node_modules/,
35 | }),
36 | json(),
37 | ],
38 | };
39 |
40 | if (process.env.NODE_ENV === 'production') {
41 | config.plugins.push(uglify());
42 | }
43 |
44 | export default config;
45 |
--------------------------------------------------------------------------------
/src/withContentful.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Head from 'next/head';
4 | import { getDataFromTree } from 'next-utils';
5 | import { ContentfulProvider } from 'react-contentful';
6 | import { getDisplayName } from 'react-hoc-helpers';
7 | import { stringify } from 'flatted';
8 |
9 | import initContentful from './initContentful';
10 |
11 | export default function withContentful({ accessToken, host, space, environment, locale }) {
12 | return (ComposedComponent) => {
13 | const propTypes = {
14 | contentfulState: PropTypes.shape(),
15 | };
16 |
17 | const defaultProps = {
18 | contentfulState: null,
19 | };
20 |
21 | const Contentful = class extends React.Component {
22 | static displayName = `withContentful(${getDisplayName(ComposedComponent)})`;
23 |
24 | constructor(props) {
25 | super(props);
26 |
27 | this.contentfulClient = initContentful(props.contentfulState);
28 | }
29 |
30 | static async getInitialProps(props) {
31 | const { Component, router, ctx } = props;
32 |
33 | let composedInitialProps = {};
34 | if (ComposedComponent.getInitialProps) {
35 | composedInitialProps = await ComposedComponent.getInitialProps(props);
36 | }
37 |
38 | // Run all Contentful queries in the component tree
39 | // and extract the resulting data
40 | const contentful = initContentful({
41 | accessToken,
42 | host,
43 | space,
44 | environment,
45 | });
46 |
47 | if (!process.browser) {
48 | try {
49 | // Run all Contentful queries
50 | await getDataFromTree(
51 |
52 |
59 |
60 | );
61 | } catch (error) {
62 | // Prevent Contentful Client errors from crashing SSR.
63 | // Handle them in components via the data.error prop:
64 | if (process.env.NODE_ENV === 'development') {
65 | console.log(error);
66 | }
67 | }
68 |
69 | // getDataFromTree does not call componentWillUnmount
70 | // head side effect therefore need to be cleared manually
71 | Head.rewind();
72 | }
73 |
74 | // Pass in the Contentful client credentials and initial cache state
75 | const contentfulState = {
76 | accessToken,
77 | host,
78 | space,
79 | environment,
80 | cache: stringify(contentful.cache.extract()),
81 | };
82 |
83 | return {
84 | ...composedInitialProps,
85 | contentfulState,
86 | };
87 | }
88 |
89 | render() {
90 | return (
91 |
92 |
93 |
94 | );
95 | }
96 | };
97 |
98 | Contentful.propTypes = propTypes;
99 | Contentful.defaultProps = defaultProps;
100 |
101 | return Contentful;
102 | };
103 | }
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next Contentful
2 |
3 | [](https://www.npmjs.com/package/next-contentful)
4 | [](LICENSE)
5 | [](https://www.npmjs.com/package/next-contentful)
6 |
7 | React library for integrating [`react-contentful`](https://github.com/ryanhefner/react-contentful)
8 | into the server-side rendering of your Next.js app.
9 |
10 | ## Install
11 |
12 | Via [npm](https://npmjs.com/package/next-contentful)
13 | ```sh
14 | npm install --save next-contentful
15 | ```
16 |
17 | Via [Yarn](https://yarn.fyi/next-contentful)
18 | ```sh
19 | yarn add next-contentful
20 | ```
21 |
22 | ## How to use
23 |
24 | To use `next-contentful`, just set the parameters that will be used by the
25 | `ContentfulClient` from [`react-contentful`](https://github.com/ryanhefner/react-contentful)
26 | and wraps your Next.js app in a `ContentfulProvider` and handles initiating both
27 | the `ContentfulClient` for both SSR/requests and the browser client.
28 |
29 | Any `Query` instance that appear in your React tree will be queued and requested
30 | server-side and included in the initial state to make reduce the requests being
31 | made by the client and results in your Next/React app rendering faster client-side.
32 |
33 | Here’s an example of how it is used:
34 |
35 | ```js
36 | import App, { Container } from 'next/app';
37 | import React from 'react';
38 | import { withContentful } from 'next-contentful';
39 |
40 | // Contentful config properties
41 | const space = '[CONTENTFUL SPACE]';
42 | const accessToken = '[CONTENTFUL ACCESS TOKEN]';
43 | const host = 'cdn.contentful.com';
44 | const locale = 'es-US';
45 |
46 | class MyApp extends App {
47 | static async getInitialProps({ Component, ctx, router }) {
48 | let pageProps = {};
49 |
50 | if (Component.getInitialProps) {
51 | pageProps = await Component.getInitialProps({ ctx });
52 | }
53 |
54 | return { pageProps };
55 | }
56 |
57 | render() {
58 | const {
59 | Component,
60 | pageProps,
61 | store,
62 | } = this.props;
63 |
64 | return (
65 |
66 |
67 |
68 | );
69 | }
70 | }
71 |
72 | export default withContentful({
73 | space,
74 | accessToken,
75 | host, // Optional: Defaults to 'cdn.contentful.com'
76 | locale, // Optional: Defaults to `en-US`
77 | })(MyApp);
78 |
79 | ```
80 |
81 | ### `withContentful`
82 |
83 | Higher-order component that makes it easy to quickly add Contentful support to your Next.js application by providing the setup to that allows [`react-contentful`](https://github.com/ryanhefner/react-contentful) components to be rendered both server-side and client-side within your React applications.
84 |
85 | #### Options
86 |
87 | * `space: string` - ID of the Contentful space you are working with
88 |
89 | * `accessToken: string` - Contentful access token used to access the space
90 |
91 | * `locale: string` - Default locale to apply to queries (Note: This can be overriden with locales set on the individual queries).
92 |
93 | * `host: string` - Host to use for the Contentful requests. Defaults to `cdn.contentful.com`, but could be set to `preview.contentful.com` when used with a preview token.
94 |
95 | * `environment: string` – Environment value to use for Contentful requests. Defaults to `master`.
96 |
97 | ## License
98 |
99 | [MIT](LICENSE) © [Ryan Hefner](https://www.ryanhefner.com)
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-contentful",
3 | "version": "0.5.0",
4 | "description": "React library for integrating react-contentful into the server-side rendering of your Next.js app.",
5 | "repository": "ryanhefner/next-contentful",
6 | "keywords": [
7 | "react",
8 | "next",
9 | "contentful",
10 | "react-contentful",
11 | "react-hoc",
12 | "server-side rendering",
13 | "ssr",
14 | "rendering",
15 | "server-side",
16 | "nextjs",
17 | "reactjs"
18 | ],
19 | "author": "Ryan Hefner (https://www.ryanhefner.com)",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/ryanhefner/next-contentful/issues"
23 | },
24 | "homepage": "https://github.com/ryanhefner/next-contentful#readme",
25 | "files": [
26 | "es",
27 | "src",
28 | "types",
29 | "hoc-utils.js",
30 | "index.js",
31 | "initContentful.js",
32 | "withContentful.js"
33 | ],
34 | "directories": {
35 | "lib": "/src"
36 | },
37 | "main": "index.js",
38 | "module": "es/index.js",
39 | "jsnext:main": "src/index.js",
40 | "types": "types",
41 | "scripts": {
42 | "clean": "rm -f index.js && rm -f initContentful.js && rm -f withContentful.js && rm -rf es && rm -rf umd",
43 | "prebuild": "npm run clean",
44 | "build": "node ./tools/build.js",
45 | "watch": "babel ./src -d . --ignore __mocks__,__tests__,**/*.test.js --watch",
46 | "prepare": "npm run build",
47 | "prepublishOnly": "node ./tools/build.js",
48 | "push-release": "git push origin master && git push --tags",
49 | "dtslint": "dtslint types",
50 | "test": "jest --findRelatedTests src/index.js"
51 | },
52 | "peerDependencies": {
53 | "next": "^10.0.5",
54 | "react": ">=16.3",
55 | "react-dom": ">=16.3"
56 | },
57 | "dependencies": {
58 | "@babel/runtime": "^7.8.4",
59 | "@types/react": "^16.9.23",
60 | "flatted": "^3.1.1",
61 | "next-utils": "^0.1.3",
62 | "prop-types": "^15.6.2",
63 | "react-contentful": "^2.0.4",
64 | "react-hoc-helpers": "^0.1.4"
65 | },
66 | "devDependencies": {
67 | "@babel/cli": "^7.8.4",
68 | "@babel/core": "^7.8.6",
69 | "@babel/plugin-proposal-class-properties": "^7.8.3",
70 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3",
71 | "@babel/plugin-transform-runtime": "^7.8.3",
72 | "@babel/preset-env": "^7.8.6",
73 | "@babel/preset-react": "^7.8.3",
74 | "babel-core": "^7.0.0-bridge.0",
75 | "babel-jest": "^26.6.3",
76 | "babel-plugin-dev-expression": "^0.2.2",
77 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
78 | "babel-polyfill": "^6.26.0",
79 | "coveralls": "^3.0.9",
80 | "dtslint": "^4.0.6",
81 | "enzyme": "^3.11.0",
82 | "enzyme-adapter-react-16": "^1.15.2",
83 | "greenkeeper-lockfile": "^1.15.1",
84 | "gzip-size": "^6.0.0",
85 | "jest": "^26.6.3",
86 | "jest-enzyme": "^7.1.2",
87 | "jsdom": "^16.2.0",
88 | "next": "10.0.5",
89 | "pretty-bytes": "^5.3.0",
90 | "react": "^16.13.0",
91 | "react-dom": "^16.13.0",
92 | "react-test-renderer": "^16.13.0",
93 | "regenerator-runtime": "^0.13.3",
94 | "rollup": "^2.38.0",
95 | "rollup-plugin-babel": "^4.3.3",
96 | "rollup-plugin-commonjs": "^10.1.0",
97 | "rollup-plugin-json": "^4.0.0",
98 | "rollup-plugin-node-resolve": "^5.2.0",
99 | "rollup-plugin-uglify": "^6.0.4",
100 | "rollup-watch": "^4.3.1",
101 | "typescript": "^4.1.3"
102 | },
103 | "jest": {
104 | "collectCoverage": true,
105 | "collectCoverageFrom": [
106 | "src/**/*.js",
107 | "!src/**/*.test.js"
108 | ],
109 | "setupFiles": [
110 | "/tools/jest-setup.js"
111 | ],
112 | "setupFilesAfterEnv": [
113 | "./node_modules/jest-enzyme/lib/index.js"
114 | ],
115 | "testURL": "http://localhost"
116 | }
117 | }
118 |
--------------------------------------------------------------------------------