├── README.md ├── packages └── sdk │ ├── .npmignore │ ├── src │ ├── utils │ │ ├── get-nested-object.js │ │ ├── date.js │ │ ├── find-child-by-id.js │ │ ├── get-configuration-for-path.js │ │ ├── image-url.js │ │ ├── link-rewriter.js │ │ ├── add-html-comment.js │ │ ├── fetch.js │ │ ├── create-link.js │ │ └── cms-urls.js │ ├── cms-components │ │ └── core │ │ │ ├── placeholder.js │ │ │ ├── undefined.js │ │ │ ├── component.js │ │ │ ├── cms-edit-button.js │ │ │ ├── render-cms-component.js │ │ │ ├── content-component-wrapper.js │ │ │ ├── container-item.js │ │ │ ├── container.js │ │ │ └── page.js │ ├── context.js │ └── index.js │ ├── babel.config.js │ ├── rollup.config.js │ ├── package.json │ └── README.md ├── examples ├── server-side-rendered │ ├── .babelrc │ ├── .env │ ├── src │ │ ├── routes.js │ │ ├── index.js │ │ ├── next.config.js │ │ ├── static │ │ │ └── custom.css │ │ ├── components │ │ │ ├── menu-item.js │ │ │ ├── menu.js │ │ │ ├── news-item.js │ │ │ ├── banner.js │ │ │ ├── content.js │ │ │ └── news-list.js │ │ └── pages │ │ │ ├── _document.js │ │ │ └── index.js │ ├── package.json │ └── README.md └── client-side-rendered │ ├── .env │ ├── server.js │ ├── package.json │ ├── public │ ├── static │ │ └── custom.css │ └── index.html │ ├── src │ ├── components │ │ ├── news-item.js │ │ ├── banner.js │ │ ├── content.js │ │ ├── menu.js │ │ └── news-list.js │ └── index.js │ └── README.md ├── .gitignore ├── Procfile ├── .editorconfig ├── .travis.yml ├── NOTICE ├── .eslintrc ├── package.json └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | packages/sdk/README.md -------------------------------------------------------------------------------- /packages/sdk/.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc 2 | babel.config.js 3 | rollup.config.js 4 | -------------------------------------------------------------------------------- /examples/server-side-rendered/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel" 5 | ] 6 | ] 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /packages/*/dist/ 3 | /examples/*/build/ 4 | /examples/server-side-rendered/src/.next/ 5 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | # start the CSR or SSR example, depending on an environment variable 2 | web: yarn run start:example-$CSR_OR_SSR 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /examples/client-side-rendered/.env: -------------------------------------------------------------------------------- 1 | # The PUBLIC_URL variable will set a prefix to the application assets. 2 | # See https://create-react-app.dev/docs/using-the-public-folder for more information. 3 | # PUBLIC_URL=http://localhost:3000 4 | 5 | REACT_APP_BR_ORIGIN=http://localhost:8080 6 | REACT_APP_BR_CONTEXT_PATH=site 7 | REACT_APP_BR_CHANNEL_PATH= 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | 5 | install: 6 | - yarn 7 | 8 | script: 9 | - yarn workspace bloomreach-experience-react-sdk lint 10 | - yarn build 11 | 12 | deploy: 13 | edge: true 14 | provider: npm 15 | cleanup: false 16 | src: packages/sdk 17 | api_token: $NPM_AUTH_TOKEN 18 | on: 19 | branch: master 20 | tags: true 21 | -------------------------------------------------------------------------------- /examples/server-side-rendered/.env: -------------------------------------------------------------------------------- 1 | # The PUBLIC_URL variable will set the 'assetPrefix' option in Next. By default it is set to '/' 2 | # You should use this variable in your templates as well, when linking static resources like style sheets and images: 3 | # e.g. 4 | # PUBLIC_URL=http://localhost:3000 5 | 6 | BR_ORIGIN=http://localhost:8080 7 | BR_CONTEXT_PATH=site 8 | BR_CHANNEL_PATH= 9 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Bloomreach Experience React SDK 2 | Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | 4 | This product includes software developed by: 5 | Hippo B.V., Amsterdam, The Netherlands (http://www.onehippo.com/); 6 | The Apache Software Foundation (http://www.apache.org/). 7 | 8 | NOTICE: Only our own original work is licensed under the terms of the 9 | Apache License Version 2.0. The licenses of some libraries might impose 10 | different redistribution or general licensing terms than those stated in the 11 | Apache License. Users and redistributors are hereby requested to verify these 12 | conditions and agree upon them. 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "airbnb-base", 5 | "plugin:react/recommended" 6 | ], 7 | "env": { 8 | "browser": true 9 | }, 10 | "rules": { 11 | "class-methods-use-this": "off", 12 | "global-require": "off", 13 | "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.spec.js"]}], 14 | "import/prefer-default-export": "off", 15 | "linebreak-style": "off", 16 | "max-len": ["error", { "code": 120 }], 17 | "no-console": "off", 18 | "no-param-reassign": "off", 19 | "no-prototype-builtins": "off", 20 | "no-restricted-properties": "off", 21 | "no-underscore-dangle": "off", 22 | "react/prop-types": "off", 23 | "react/react-in-jsx-scope": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/routes.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const routes = require('next-routes')(); 18 | 19 | routes 20 | .add('index', '/(.*)'); 21 | 22 | module.exports = routes; 23 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/get-nested-object.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export default function getNestedObject(nestedObj, pathArr) { 18 | return pathArr.reduce((obj, key) => ((obj && obj[key] !== 'undefined') ? obj[key] : null), nestedObj); 19 | } 20 | -------------------------------------------------------------------------------- /packages/sdk/babel.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | presets: [ 19 | '@babel/preset-env', 20 | '@babel/preset-react', 21 | ], 22 | plugins: [ 23 | '@babel/plugin-proposal-object-rest-spread', 24 | ['babel-plugin-transform-async-to-promises', { inlineHelpers: true }], 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "repository": "https://github.com/bloomreach/experience-react-sdk", 4 | "bugs": "https://issues.onehippo.com/projects/CMS", 5 | "homepage": "https://github.com/bloomreach/experience-react-sdk", 6 | "author": "Bloomreach B.V.", 7 | "license": "Apache-2.0", 8 | "workspaces": { 9 | "packages": [ 10 | "packages/*", 11 | "examples/*" 12 | ], 13 | "nohoist": [ 14 | "**/eslint-formatter-friendly" 15 | ] 16 | }, 17 | "scripts": { 18 | "build": "yarn workspaces run build", 19 | "start:example-csr": "yarn --cwd examples/client-side-rendered start", 20 | "start:example-ssr": "yarn --cwd examples/server-side-rendered start" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^6.3.0", 24 | "eslint-config-airbnb-base": "^14.0.0", 25 | "eslint-formatter-friendly": "^7.0.0", 26 | "eslint-plugin-babel": "^5.3.0", 27 | "eslint-plugin-import": "^2.18.2", 28 | "eslint-plugin-react": "^7.14" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/placeholder.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | 19 | export default class Placeholder extends React.Component { 20 | // placeholder component is used for when components data is not set 21 | // this is the case when a new component is added to a container 22 | render() { 23 | return

Click to configure { this.props.name }

; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/client-side-rendered/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const express = require('express'); 18 | const path = require('path'); 19 | const port = process.env.PORT || 3000; 20 | const app = express(); 21 | 22 | app.use(express.static(path.join(__dirname, 'build'))); 23 | 24 | app.get('/*', function (req, res) { 25 | res.sendFile(path.join(__dirname, 'build', 'index.html')); 26 | }); 27 | app.listen(port); 28 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/undefined.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | 19 | export default class UndefinedComponent extends React.Component { 20 | // fallback component when unknown/undefined component type is used 21 | render() { 22 | return ( 23 |

24 | Component { this.props.name } not defined 25 |

26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const next = require('next'); 18 | const { createServer } = require('http'); 19 | const routes = require('./routes'); 20 | 21 | const app = next({ 22 | dev: process.env.NODE_ENV !== 'production', 23 | dir: './src', 24 | }); 25 | const handler = routes.getRequestHandler(app); 26 | 27 | app.prepare().then(() => { 28 | createServer(handler).listen(process.env.PORT || 3000); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/next.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const dotenv = require('dotenv').config(); 18 | 19 | if (dotenv.error) { 20 | throw dotenv.error; 21 | } 22 | 23 | module.exports = { 24 | assetPrefix: process.env.PUBLIC_URL || '/', 25 | publicRuntimeConfig: { 26 | brOrigin: process.env.BR_ORIGIN, 27 | brContextPath: process.env.BR_CONTEXT_PATH, 28 | brChannelPath: process.env.BR_CHANNEL_PATH, 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /examples/server-side-rendered/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloomreach-experience-react-sdk-ssr-example", 3 | "version": "0.6.4", 4 | "description": "Example server-side React App for the Bloomreach Experience SDK for React", 5 | "private": true, 6 | "author": "Bloomreach B.V.", 7 | "license": "Apache-2.0", 8 | "dependencies": { 9 | "bloomreach-experience-react-sdk": "^0.6.4", 10 | "dotenv": "^8.2.0", 11 | "isomorphic-unfetch": "^3.0.0", 12 | "next": "^9.1.6", 13 | "next-routes": "^1.4.2", 14 | "react": "^16.12.0", 15 | "react-dom": "^16.12.0" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^6.7.2", 19 | "eslint-config-airbnb-base": "^14.0.0", 20 | "eslint-formatter-friendly": "^7.0.0", 21 | "eslint-plugin-babel": "^5.3.0", 22 | "eslint-plugin-import": "^2.19.1", 23 | "eslint-plugin-react": "^7.17" 24 | }, 25 | "scripts": { 26 | "dev": "node src/index.js", 27 | "build": "next build src/", 28 | "lint": "eslint src/ --format node_modules/eslint-formatter-friendly", 29 | "start": "NODE_ENV=production node src/index.js" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/client-side-rendered/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloomreach-experience-react-sdk-csr-example", 3 | "version": "0.6.4", 4 | "description": "Example client-side React App for the Bloomreach Experience SDK for React", 5 | "private": true, 6 | "dependencies": { 7 | "bloomreach-experience-react-sdk": "^0.6.4", 8 | "react": "^16.12.0", 9 | "react-dom": "^16.12.0", 10 | "react-router-dom": "^5.1.2", 11 | "react-scripts": "^3.3.0" 12 | }, 13 | "scripts": { 14 | "dev": "react-scripts start", 15 | "build": "react-scripts build", 16 | "start": "node server.js", 17 | "test": "react-scripts test --env=jsdom", 18 | "eject": "react-scripts eject" 19 | }, 20 | "author": "Bloomreach B.V.", 21 | "license": "Apache-2.0", 22 | "homepage": ".", 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | }, 35 | "devDependencies": { 36 | "express": "^4.17.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/static/custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | padding-bottom: 0; 19 | } 20 | 21 | .container { 22 | padding-top: 2rem; 23 | } 24 | 25 | footer { 26 | margin-top: 30px; 27 | } 28 | 29 | .jumbotron img { 30 | width: 100%; 31 | } 32 | 33 | .navbar .navbar-brand { 34 | margin-right: 3rem; 35 | } 36 | 37 | .navbar-nav .nav-link { 38 | text-transform: capitalize; 39 | } 40 | 41 | .blog-post-date { 42 | margin-right: 10px; 43 | } 44 | 45 | .has-edit-button { 46 | position: relative; 47 | } 48 | -------------------------------------------------------------------------------- /examples/client-side-rendered/public/static/custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | padding-bottom: 0; 19 | } 20 | 21 | .container { 22 | padding-top: 2rem; 23 | } 24 | 25 | footer { 26 | margin-top: 30px; 27 | } 28 | 29 | .jumbotron img { 30 | width: 100%; 31 | } 32 | 33 | .navbar .navbar-brand { 34 | margin-right: 3rem; 35 | } 36 | 37 | .navbar-nav .nav-link { 38 | text-transform: capitalize; 39 | } 40 | 41 | .blog-post-date { 42 | margin-right: 10px; 43 | } 44 | 45 | .has-edit-button { 46 | position: relative; 47 | } 48 | -------------------------------------------------------------------------------- /packages/sdk/src/context.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | 19 | export const PageModelContext = React.createContext({}); 20 | export const PreviewContext = React.createContext(''); 21 | export const ComponentDefinitionsContext = React.createContext({}); 22 | export const CreateLinkContext = React.createContext(); 23 | 24 | export function withPageModel(Component) { 25 | return function PageModelComponent(props) { 26 | return ( 27 | 28 | {(pageModel) => } 29 | 30 | ); 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/date.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const MONTHS = [ 18 | 'January', 19 | 'February', 20 | 'March', 21 | 'April', 22 | 'May', 23 | 'June', 24 | 'July', 25 | 'August', 26 | 'September', 27 | 'October', 28 | 'November', 29 | 'December', 30 | ]; 31 | 32 | export default function parseDate(date) { 33 | const parsedDate = parseFloat(date); 34 | // eslint-disable-next-line no-restricted-globals 35 | if (isNaN(parsedDate)) { 36 | return null; 37 | } 38 | 39 | const dateObj = new Date(parsedDate); 40 | 41 | return `${MONTHS[dateObj.getMonth()]} ${dateObj.getDate()}, ${dateObj.getFullYear()}`; 42 | } 43 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/find-child-by-id.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // returns parent and index of child referenced by ID, 18 | // so that we can easily replace the child 19 | export default function findChildById(object, id, parent, idx) { 20 | const props = Object.keys(object); 21 | // eslint-disable-next-line no-plusplus 22 | for (let i = 0; i < props.length; i++) { 23 | const prop = props[i]; 24 | if (typeof object[prop] === 'object' && object[prop] !== null) { 25 | const result = findChildById(object[prop], id, object, prop); 26 | if (result) { 27 | return result; 28 | } 29 | } else if (prop === 'id' && object.id === id) { 30 | return { parent, idx }; 31 | } 32 | } 33 | 34 | return null; 35 | } 36 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/component.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import CmsContainer from './container'; 19 | 20 | export default class CmsComponent extends React.Component { 21 | render() { 22 | const { configuration } = this.props; 23 | 24 | if (!configuration || !configuration.components || !configuration.components.length) { 25 | return null; 26 | } 27 | 28 | return ( 29 | 30 | { configuration.components.map((component) => { 31 | if (component.type === 'CONTAINER_COMPONENT') { 32 | return ; 33 | } 34 | 35 | return ; 36 | }) } 37 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/components/menu-item.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { createLink } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class CmsMenuItem extends React.Component { 21 | render() { 22 | const { configuration } = this.props; 23 | 24 | if (!configuration) { 25 | return null; 26 | } 27 | 28 | const activeElm = configuration.selected ? (current) : null; 29 | // createLink takes linkText as a function so that it can contain HTML elements 30 | const linkText = () => {configuration.name}{activeElm}; 31 | const className = 'nav-link'; 32 | 33 | return ( 34 |
  • 35 | { createLink('self', configuration, linkText, className) } 36 |
  • 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /examples/client-side-rendered/public/index.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | React App 24 | 25 | 29 | 30 | 31 | 32 | 35 |
    36 |
    37 | 38 | 39 | -------------------------------------------------------------------------------- /packages/sdk/rollup.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* eslint-disable import/no-extraneous-dependencies */ 18 | import babel from 'rollup-plugin-babel'; 19 | import { terser } from 'rollup-plugin-terser'; 20 | 21 | export default [ 22 | { 23 | input: 'src/index.js', 24 | output: [ 25 | { 26 | exports: 'named', 27 | file: 'dist/bloomreach-experience-react-sdk.js', 28 | format: 'umd', 29 | name: 'BloomreachReactSdk', 30 | sourcemap: true, 31 | sourcemapFile: 'dist/bloomreach-experience-react-sdk.js.map', 32 | globals: { 33 | react: 'React', 34 | }, 35 | }, 36 | ], 37 | plugins: [ 38 | babel({ extensions: ['.js'] }), 39 | terser({ 40 | ecma: 5, 41 | mangle: false, 42 | compress: false, 43 | output: { 44 | beautify: true, 45 | comments: false, 46 | }, 47 | }), 48 | ], 49 | }, 50 | ]; 51 | -------------------------------------------------------------------------------- /examples/server-side-rendered/README.md: -------------------------------------------------------------------------------- 1 | # Example server-side React App 2 | 3 | Example server-side React app using the BloomReach Experience SDK for React. The app uses [Next.js](https://github.com/zeit/next.js) 4 | as framework for creating a server-side rendered app. 5 | 6 | ## Install and run 7 | 8 | First, download and install the [BloomReach SPA demo project](https://github.com/onehippo/hippo-demo-spa-integration) 9 | by following the instructions in the *Build Demo CMS project* section of the above link. Then run it by following the 10 | instructions in *Run Demo CMS project*. 11 | 12 | Next, install the [UrlRewriter](https://documentation.bloomreach.com/library/enterprise/enterprise-features/url-rewriter/installation.html) 13 | and configure that according to [this document](https://documentation.bloomreach.com/library/concepts/spa-plus/url-rewriter-rules.html). 14 | 15 | Then, customize `.env` file to contain a correct PUBLIC_URL path, for example: 16 | ``` 17 | PUBLIC_URL=http://localhost:3000 18 | ``` 19 | 20 | In the same `.env` file, also specify the brXM instance to fetch the page model from. The default configuration 21 | connects to `http://localhost:8080/site/`: 22 | 23 | ``` 24 | BR_ORIGIN=http://localhost:8080 25 | BR_CONTEXT_PATH=site 26 | BR_CHANNEL_PATH= 27 | ``` 28 | 29 | Finally, build and run the React app as followed: 30 | 31 | ```bash 32 | yarn 33 | yarn run dev 34 | ``` 35 | 36 | The CMS should now be accessible at , and it should render the server-side React app in preview 37 | mode in the Channel Manager. The SPA itself can be accessed directly via . 38 | -------------------------------------------------------------------------------- /packages/sdk/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // exports for fetching API and rendering 18 | export { getApiUrl } from './utils/cms-urls'; 19 | export { default as CmsPage } from './cms-components/core/page'; 20 | export { default as RenderCmsComponent } from './cms-components/core/render-cms-component'; 21 | 22 | // exports for building custom components 23 | export { default as ContentComponentWrapper } from './cms-components/core/content-component-wrapper'; 24 | export { default as CmsEditButton } from './cms-components/core/cms-edit-button'; 25 | export { default as Placeholder } from './cms-components/core/placeholder'; 26 | export { default as createLink } from './utils/create-link'; 27 | export { default as parseDate } from './utils/date'; 28 | export { default as getNestedObject } from './utils/get-nested-object'; 29 | export { getImageUrl } from './utils/image-url'; 30 | export { getImageUrlByPath } from './utils/image-url'; 31 | export { default as parseAndRewriteLinks } from './utils/link-rewriter'; 32 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/get-configuration-for-path.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import getNestedObject from './get-nested-object'; 18 | 19 | function getConfigurationForPathSegment(pathSegment, configuration) { 20 | return configuration.components.find((component) => pathSegment === component.name || pathSegment === '*') || null; 21 | } 22 | 23 | export default function getConfigurationForPath(path, pageModel) { 24 | const pathSegments = path.split('/'); 25 | let currPath; 26 | let configuration = pageModel.page; 27 | 28 | while (getNestedObject(configuration, ['components', 0])) { 29 | // match the next path segment 30 | currPath = pathSegments.shift(); 31 | 32 | configuration = getConfigurationForPathSegment(currPath, configuration); 33 | 34 | if (configuration && pathSegments.length === 0) { 35 | // this was the last path segment and we retrieved configuration, so we can return the configuration 36 | return configuration; 37 | } 38 | } 39 | 40 | return null; 41 | } 42 | -------------------------------------------------------------------------------- /packages/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloomreach-experience-react-sdk", 3 | "version": "0.6.4", 4 | "description": "Bloomreach Experience SDK for React", 5 | "keywords": [ 6 | "bloomreach", 7 | "sdk", 8 | "react" 9 | ], 10 | "repository": { 11 | "url": "https://github.com/bloomreach/experience-react-sdk", 12 | "type": "git", 13 | "directory": "packages/sdk" 14 | }, 15 | "bugs": "https://issues.onehippo.com/projects/CMS", 16 | "homepage": "https://github.com/bloomreach/experience-react-sdk", 17 | "dependencies": { 18 | "axios": "^0.18.0", 19 | "jsonpointer": "^4.0.1", 20 | "path-to-regexp": "^2.2.1", 21 | "react-html-parser": "^2.0.2" 22 | }, 23 | "peerDependencies": { 24 | "react": "^16.2.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.7", 28 | "@babel/plugin-proposal-object-rest-spread": "^7.7", 29 | "@babel/preset-env": "^7.7", 30 | "@babel/preset-react": "^7.7", 31 | "babel-plugin-transform-async-to-promises": "^0.8", 32 | "eslint": "^6.7.2", 33 | "eslint-config-airbnb-base": "^14.0.0", 34 | "eslint-formatter-friendly": "^7.0.0", 35 | "eslint-plugin-babel": "^5.3.0", 36 | "eslint-plugin-import": "^2.19.1", 37 | "eslint-plugin-react": "^7.17", 38 | "react": "^16.8", 39 | "rollup": "^1.27", 40 | "rollup-plugin-babel": "^4.3", 41 | "rollup-plugin-terser": "^5.1" 42 | }, 43 | "main": "dist/bloomreach-experience-react-sdk.js", 44 | "scripts": { 45 | "lint": "eslint src/ --format node_modules/eslint-formatter-friendly", 46 | "build": "rollup -c rollup.config.js" 47 | }, 48 | "author": "Bloomreach B.V.", 49 | "license": "Apache-2.0" 50 | } 51 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/components/menu.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { CmsEditButton, getNestedObject } from 'bloomreach-experience-react-sdk'; 19 | 20 | import CmsMenuItem from './menu-item'; 21 | 22 | export default class CmsMenu extends React.Component { 23 | renderMenu(configuration) { 24 | return configuration.models.menu.siteMenuItems.map( 25 | (menuItem) => , 26 | ); 27 | } 28 | 29 | render() { 30 | const { configuration, preview } = this.props; 31 | 32 | if (!getNestedObject(configuration, ['models', 'menu', 'siteMenuItems', 0])) { 33 | return null; 34 | } 35 | 36 | const menuConfiguration = getNestedObject(configuration, ['models', 'menu']); 37 | const editButton = preview ? : null; 38 | 39 | return ( 40 |
      41 | { editButton && editButton } 42 | { this.renderMenu(configuration) } 43 |
    44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/client-side-rendered/src/components/news-item.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { createLink, parseDate } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class NewsItem extends React.Component { 21 | render() { 22 | const { content, manageContentButton } = this.props; 23 | // createLink takes linkText as a function so that it can contain HTML elements 24 | const linkText = () => content.title; 25 | 26 | return ( 27 |
    28 | { manageContentButton } 29 |

    30 | { createLink('self', content, linkText, null) } 31 |

    32 |

    33 | { content.date 34 | && {parseDate(content.date)} 35 | } 36 | { content.author 37 | && {content.author} 38 | } 39 |

    40 | { content.introduction &&

    {content.introduction}

    } 41 |
    42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/components/news-item.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { createLink, parseDate } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class NewsItem extends React.Component { 21 | render() { 22 | const { content } = this.props; 23 | const { manageContentButton } = this.props; 24 | // createLink takes linkText as a function so that it can contain HTML elements 25 | const linkText = () => content.title; 26 | 27 | return ( 28 |
    29 | { manageContentButton } 30 |

    31 | { createLink('self', content, linkText, null) } 32 |

    33 |

    34 | { content.date 35 | && {parseDate(content.date)} 36 | } 37 | { content.author 38 | && {content.author} 39 | } 40 |

    41 | { content.introduction &&

    {content.introduction}

    } 42 |
    43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/client-side-rendered/README.md: -------------------------------------------------------------------------------- 1 | # Example client-side React App 2 | 3 | Example client-side React app using the BloomReach Experience SDK for React. The app is created using [create-react-app](https://github.com/facebook/create-react-app). 4 | 5 | ## Install and run 6 | 7 | First, download and install the [BloomReach SPA demo project](https://github.com/onehippo/hippo-demo-spa-integration) 8 | by following the instructions in the *Build Demo CMS project* section of the above link. Then run it by following the 9 | instructions in *Run Demo CMS project*. 10 | 11 | Next, install the [UrlRewriter](https://documentation.bloomreach.com/library/enterprise/enterprise-features/url-rewriter/installation.html) 12 | and configure that according to [this document](https://documentation.bloomreach.com/library/concepts/spa-plus/url-rewriter-rules.html). 13 | 14 | Then, customize `.env` file to contain a correct [PUBLIC_URL](https://create-react-app.dev/docs/using-the-public-folder) path, for example: 15 | ``` 16 | PUBLIC_URL=http://localhost:3000 17 | ``` 18 | 19 | Beware of [this issue](https://github.com/facebook/create-react-app/pull/7259). The PUBLIC_URL may not work in development mode. 20 | 21 | In the same `.env` file, also specify the brXM instance to fetch the page model from. The default configuration 22 | connects to `http://localhost:8080/site/`: 23 | 24 | ``` 25 | REACT_APP_BR_ORIGIN=http://localhost:8080 26 | REACT_APP_BR_CONTEXT_PATH=site 27 | REACT_APP_BR_CHANNEL_PATH= 28 | ``` 29 | 30 | Finally, build and run the React app as follows: 31 | 32 | ```bash 33 | yarn 34 | yarn run build 35 | yarn run start 36 | ``` 37 | 38 | The CMS should now be accessible at , and it should render the client-side React app in preview 39 | mode in the Channel Manager. The SPA itself can be accessed directly via . 40 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/pages/_document.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import Document, { Head, Main, NextScript } from 'next/document'; 18 | 19 | export default class DefaultDocument extends Document { 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | React App 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 |
    40 |
    41 |
    42 | 43 | 44 | 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/client-side-rendered/src/components/banner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { createLink, getImageUrl, parseAndRewriteLinks } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class Banner extends React.Component { 21 | render() { 22 | const { content, manageContentButton, preview } = this.props; 23 | const image = getImageUrl(content.image, this.props.pageModel, preview); 24 | 25 | let contentHtml; 26 | if (content.content && content.content.value) { 27 | contentHtml = parseAndRewriteLinks(content.content.value, preview); 28 | } 29 | 30 | const link = content.link ? content.link.$ref : null; 31 | // createLink takes linkText as a function so that it can contain HTML elements 32 | const linkText = () => 'Learn more'; 33 | const className = 'btn btn-primary btn-lg'; 34 | 35 | return ( 36 |
    37 | { manageContentButton } 38 | { content.title &&

    {content.title}

    } 39 | { image &&
    40 | {content.title}/ 41 |
    } 42 | { contentHtml } 43 |

    { link && createLink('ref', link, linkText, className) }

    44 |
    45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/components/banner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { createLink, getImageUrl, parseAndRewriteLinks } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class Banner extends React.Component { 21 | render() { 22 | const { content, manageContentButton, preview } = this.props; 23 | const image = getImageUrl(content.image, this.props.pageModel, preview); 24 | 25 | let contentHtml; 26 | if (content.content && content.content.value) { 27 | contentHtml = parseAndRewriteLinks(content.content.value, preview); 28 | } 29 | 30 | const link = content.link ? content.link.$ref : null; 31 | // createLink takes linkText as a function so that it can contain HTML elements 32 | const linkText = () => 'Learn more'; 33 | const className = 'btn btn-primary btn-lg'; 34 | 35 | return ( 36 |
    37 | { manageContentButton && manageContentButton } 38 | { content.title &&

    {content.title}

    } 39 | { image &&
    40 | {content.title}/ 41 |
    } 42 | { contentHtml } 43 |

    { link && createLink('ref', link, linkText, className) }

    44 |
    45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/components/content.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { getImageUrl, parseAndRewriteLinks, parseDate } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class Content extends React.Component { 21 | render() { 22 | const { content, manageContentButton, preview } = this.props; 23 | const image = getImageUrl(content.image, this.props.pageModel, preview); 24 | 25 | let contentHtml; 26 | if (content.content && content.content.value) { 27 | contentHtml = parseAndRewriteLinks(content.content.value, preview); 28 | } 29 | 30 | return ( 31 |
    32 | { manageContentButton } 33 |

    {content.title}

    34 |

    35 | { content.date 36 | && {parseDate(content.date)} 37 | } 38 | { content.author 39 | && {content.author} 40 | } 41 |

    42 | { content.introduction &&

    {content.introduction}

    } 43 | { image &&
    44 | {content.title}/ 45 |
    } 46 | { contentHtml } 47 |
    48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/client-side-rendered/src/components/content.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { getImageUrl, parseAndRewriteLinks, parseDate } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class Content extends React.Component { 21 | render() { 22 | const { content, manageContentButton, preview } = this.props; 23 | const image = getImageUrl(content.image, this.props.pageModel, preview); 24 | 25 | let contentHtml; 26 | if (content.content && content.content.value) { 27 | contentHtml = parseAndRewriteLinks(content.content.value, preview); 28 | } 29 | 30 | return ( 31 |
    32 | { manageContentButton } 33 |

    {content.title}

    34 |

    35 | { content.date 36 | && {parseDate(content.date)} 37 | } 38 | { content.author 39 | && {content.author} 40 | } 41 |

    42 | { content.introduction 43 | &&

    {content.introduction}

    44 | } 45 | { image 46 | &&
    47 | {content.title}/ 48 |
    49 | } 50 | { contentHtml && contentHtml } 51 |
    52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/image-url.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import jsonpointer from 'jsonpointer'; 18 | import globalCmsUrls, { FULLY_QUALIFIED_LINK } from './cms-urls'; 19 | import getNestedObject from './get-nested-object'; 20 | 21 | export function getImageUrl(imageRef, pageModel, preview, variant) { 22 | // get image reference 23 | let imageUuid; 24 | if (imageRef && imageRef.$ref) { 25 | imageUuid = imageRef.$ref; 26 | } 27 | 28 | // get serialized image via reference 29 | let image; 30 | if (imageUuid && (typeof imageUuid === 'string' || imageUuid instanceof String)) { 31 | image = jsonpointer.get(pageModel, imageUuid); 32 | } 33 | 34 | // build URL 35 | let imageUrl = variant && getNestedObject(image, [variant]) 36 | ? getNestedObject(image, [variant, '_links', 'site', 'href']) 37 | : getNestedObject(image, ['_links', 'site', 'href']); 38 | 39 | if (imageUrl && !imageUrl.match(FULLY_QUALIFIED_LINK)) { 40 | imageUrl = globalCmsUrls[preview ? 'preview' : 'live'].baseUrl + imageUrl; 41 | } 42 | 43 | return imageUrl; 44 | } 45 | 46 | export function getImageUrlByPath(imagePath, variant, preview) { 47 | const cmsUrls = globalCmsUrls[preview ? 'preview' : 'live']; 48 | 49 | let imageUrl = cmsUrls.baseUrl; 50 | 51 | if (cmsUrls.contextPath) { 52 | imageUrl += `/${cmsUrls.contextPath}`; 53 | } 54 | 55 | imageUrl += '/binaries'; 56 | 57 | if (variant) { 58 | imageUrl += `/${variant}`; 59 | } 60 | 61 | imageUrl += imagePath; 62 | 63 | return imageUrl; 64 | } 65 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/cms-edit-button.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { addBeginComment } from '../../utils/add-html-comment'; 19 | import getNestedObject from '../../utils/get-nested-object'; 20 | 21 | export default class CmsEditButton extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | 25 | this.placeholder = React.createRef(); 26 | } 27 | 28 | addButton(htmlElm) { 29 | const { configuration, preview } = this.props; 30 | addBeginComment(htmlElm, 'afterbegin', configuration, preview); 31 | } 32 | 33 | shouldComponentUpdate(nextProps) { 34 | const path = ['_meta', 'beginNodeSpan', 0, 'data']; 35 | 36 | return getNestedObject(this.props.configuration, path) !== getNestedObject(nextProps.configuration, path); 37 | } 38 | 39 | componentDidMount() { 40 | if (!this.placeholder.current) { 41 | return; 42 | } 43 | 44 | this.addButton(this.placeholder.current); 45 | } 46 | 47 | componentDidUpdate() { 48 | const placeholder = this.placeholder.current; 49 | if (!placeholder) { 50 | return; 51 | } 52 | 53 | Array.from(placeholder.childNodes) 54 | .forEach((node) => node.remove()); 55 | placeholder.removeAttribute('class'); 56 | this.addButton(placeholder); 57 | } 58 | 59 | render() { 60 | if (!this.props.preview) { 61 | return null; 62 | } 63 | 64 | return ( 65 |
    66 | 67 |
    68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/client-side-rendered/src/components/menu.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { CmsEditButton, createLink, getNestedObject } from 'bloomreach-experience-react-sdk'; 19 | 20 | export default class CmsMenu extends React.Component { 21 | renderMenu(configuration) { 22 | return configuration.models.menu.siteMenuItems.map( 23 | menuItem => , 24 | ); 25 | } 26 | 27 | render() { 28 | const { configuration, preview } = this.props; 29 | 30 | if (!getNestedObject(configuration, ['models', 'menu', 'siteMenuItems', 0])) { 31 | return null; 32 | } 33 | 34 | const menuConfiguration = getNestedObject(configuration, ['models', 'menu']); 35 | const editButton = preview ? : null; 36 | 37 | return ( 38 |
      39 | { editButton } 40 | { this.renderMenu(configuration) } 41 |
    42 | ); 43 | } 44 | } 45 | 46 | class CmsMenuItem extends React.Component { 47 | render() { 48 | const { configuration } = this.props; 49 | 50 | if (!configuration) { 51 | return null; 52 | } 53 | 54 | const activeElm = configuration.selected ? (current) : null; 55 | // createLink takes linkText as a function so that it can contain HTML elements 56 | const linkText = () => {configuration.name}{activeElm}; 57 | const className = 'nav-link'; 58 | 59 | return ( 60 |
  • 61 | { createLink('self', configuration, linkText, className) } 62 |
  • 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/link-rewriter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import ReactHtmlParser from 'react-html-parser'; 19 | import globalCmsUrls, { FULLY_QUALIFIED_LINK } from './cms-urls'; 20 | import createLink from './create-link'; 21 | 22 | function getChildren(node) { 23 | if (!node.children) { 24 | return ''; 25 | } 26 | 27 | return node.children.reduce( 28 | (linkText, childNode) => linkText 29 | + getChildren(childNode) 30 | + (childNode.type === 'text' ? childNode.data : ''), 31 | '', 32 | ); 33 | } 34 | 35 | export default function parseAndRewriteLinks(html, preview) { 36 | return ReactHtmlParser(html, { 37 | // eslint-disable-next-line consistent-return 38 | transform: (node) => { 39 | if (node.type === 'tag' && node.name === 'a' && node.attribs['data-type'] 40 | && node.attribs['data-type'] === 'internal') { 41 | const { class: className, href } = node.attribs; 42 | const linkText = () => getChildren(node); 43 | const link = createLink('href', href, linkText, className); 44 | 45 | return React.cloneElement(link, { key: node.parent ? node.parent.children.indexOf(node) : 0 }); 46 | } 47 | if ( 48 | node.type === 'tag' 49 | && node.name === 'img' 50 | && node.attribs.src 51 | && !node.attribs.src.match(FULLY_QUALIFIED_LINK) 52 | ) { 53 | // transform image URLs in fully qualified URLs, so images are also loaded when requested from React app 54 | // which typically runs on a different port than CMS / HST 55 | const baseCmsUrl = globalCmsUrls[preview ? 'preview' : 'live'].baseUrl; 56 | node.attribs.src = baseCmsUrl + node.attribs.src; 57 | } 58 | }, 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/render-cms-component.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import CmsComponent from './component'; 19 | import CmsContainer from './container'; 20 | import CmsContainerItem from './container-item'; 21 | import { withPageModel, PreviewContext } from '../../context'; 22 | import getConfigurationForPath from '../../utils/get-configuration-for-path'; 23 | 24 | class RenderCmsComponent extends React.Component { 25 | renderPageComponent(configuration) { 26 | switch (configuration.type) { 27 | case 'CONTAINER_COMPONENT': 28 | return ; 29 | case 'CONTAINER_ITEM_COMPONENT': 30 | return ; 31 | default: 32 | return ; 33 | } 34 | } 35 | 36 | renderStaticComponent(renderComponent, configuration, pageModel) { 37 | return ( 38 | 39 | { (preview) => React.createElement(renderComponent, { configuration, pageModel, preview }) } 40 | 41 | ); 42 | } 43 | 44 | render() { 45 | const { path, pageModel, renderComponent } = this.props; 46 | 47 | let configuration; 48 | // render entire page if no path has been specified 49 | if (!path) { 50 | if (!pageModel) { 51 | console.log(' has no supplied page model'); 52 | return null; 53 | } 54 | 55 | configuration = pageModel.page; 56 | } else { 57 | // or lookup component configuration using supplied path 58 | configuration = getConfigurationForPath(path, pageModel); 59 | if (configuration && renderComponent) { 60 | return this.renderStaticComponent(renderComponent, configuration, pageModel); 61 | } 62 | } 63 | 64 | if (!configuration) { 65 | return null; 66 | } 67 | 68 | return this.renderPageComponent(configuration, renderComponent); 69 | } 70 | } 71 | 72 | export default withPageModel(RenderCmsComponent); 73 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/add-html-comment.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import getNestedObject from './get-nested-object'; 18 | 19 | export function addBeginComment(htmlElm, position, configuration, preview) { 20 | const beginNodeSpan = getNestedObject(configuration, ['_meta', 'beginNodeSpan', 0, 'data']); 21 | if (!preview || !htmlElm || !beginNodeSpan || htmlElm.classList.contains('cms-begin-comment-added')) { 22 | return; 23 | } 24 | 25 | htmlElm.insertAdjacentHTML(position, configuration._meta.beginNodeSpan[0].data); 26 | // adding an HTML class to ensure comments are not added more than once 27 | // this is because the comments are added through the DOM and not by React 28 | // so this function is fired on every re-render of the parent component 29 | htmlElm.classList.add('cms-begin-comment-added'); 30 | } 31 | 32 | export function addEndComment(htmlElm, position, configuration, preview) { 33 | const endNodeSpan = getNestedObject(configuration, ['_meta', 'endNodeSpan', 0, 'data']); 34 | if (!preview || !htmlElm || !endNodeSpan || htmlElm.classList.contains('cms-end-comment-added')) { 35 | return; 36 | } 37 | 38 | htmlElm.insertAdjacentHTML(position, configuration._meta.endNodeSpan[0].data); 39 | // @see comment in addBeginComment() 40 | htmlElm.classList.add('cms-end-comment-added'); 41 | } 42 | 43 | export function addBodyComments(configuration, preview) { 44 | const endNodeSpan = getNestedObject(configuration, ['_meta', 'endNodeSpan', 0, 'data']); 45 | if (!preview || !endNodeSpan) { 46 | return; 47 | } 48 | 49 | // remove comments from page meta-data element, if existing 50 | let pageMetaDataElm = document.getElementById('hst-page-meta-data'); 51 | if (pageMetaDataElm) { 52 | pageMetaDataElm.innerHTML = ''; 53 | } else { 54 | // otherwise create page-meta-data element containing page HTML comments 55 | pageMetaDataElm = document.createElement('div'); 56 | pageMetaDataElm.id = 'hst-page-meta-data'; 57 | pageMetaDataElm.style = 'display: none'; 58 | document.body.appendChild(pageMetaDataElm); 59 | } 60 | 61 | configuration._meta.endNodeSpan.forEach(({ data }) => pageMetaDataElm.insertAdjacentHTML('beforeend', data)); 62 | } 63 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import axios from 'axios'; 18 | import { buildApiUrl } from './cms-urls'; 19 | 20 | const requestConfigGet = { 21 | method: 'GET', 22 | withCredentials: true, 23 | }; 24 | 25 | const requestConfigPost = { 26 | method: 'POST', 27 | withCredentials: true, 28 | headers: { 29 | 'Content-Type': 'application/x-www-form-urlencoded', 30 | }, 31 | }; 32 | 33 | async function fetchUrl(url, requestConfig) { 34 | try { 35 | const { data } = await axios(url, requestConfig); 36 | 37 | return data; 38 | } catch (error) { 39 | if (error.response) { 40 | // The request was made and the server responded with a status code 41 | // that falls out of the range of 2xx 42 | console.log(`Error! Status code ${error.response.status} while fetching CMS page data for URL: ${url}`); 43 | console.log(error.response.data); 44 | console.log(error.response.headers); 45 | } else if (error.request) { 46 | // The request was made but no response was received 47 | // `error.request` is an instance of XMLHttpRequest in the browser and an instance of 48 | // http.ClientRequest in node.js 49 | console.log(error.request); 50 | } else { 51 | // Something happened in setting up the request that triggered an Error 52 | console.log(`Error while fetching CMS page data for URL:${url}`, error.message); 53 | } 54 | 55 | console.log(error.config); 56 | } 57 | 58 | return null; 59 | } 60 | 61 | export function fetchCmsPage(pathInfo, query, preview, cmsUrls) { 62 | const url = buildApiUrl(pathInfo, query, preview, null, cmsUrls); 63 | return fetchUrl(url, requestConfigGet); 64 | } 65 | 66 | // from rendering.service.js 67 | function toUrlEncodedFormData(json) { 68 | return Object.keys(json) 69 | .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(json[key])}`) 70 | .join('&'); 71 | } 72 | 73 | export function fetchComponentUpdate(pathInfo, query, preview, componentId, body) { 74 | const requestConfig = { data: toUrlEncodedFormData(body), ...requestConfigPost }; 75 | const url = buildApiUrl(pathInfo, query, preview, componentId); 76 | 77 | return fetchUrl(url, requestConfig); 78 | } 79 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/create-link.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import jsonpointer from 'jsonpointer'; 19 | import getNestedObject from './get-nested-object'; 20 | import { CreateLinkContext, PageModelContext } from '../context'; 21 | 22 | function isString(value) { 23 | return typeof value === 'string' || value instanceof String; 24 | } 25 | 26 | function _createLink(linkType, link, linkText, className, externalCreateLinkFunction, pageModel) { 27 | let href = null; 28 | let internalLink = null; 29 | 30 | // eslint-disable-next-line default-case 31 | switch (linkType) { 32 | case 'self': 33 | href = getNestedObject(link, ['_links', 'site', 'href']); 34 | internalLink = getNestedObject(link, ['_links', 'site', 'type']); 35 | break; 36 | 37 | case 'ref': 38 | if (!link || !isString(link)) { 39 | break; 40 | } 41 | // eslint-disable-next-line no-case-declarations 42 | const linkedContent = jsonpointer.get(pageModel, link); 43 | if (linkedContent) { 44 | href = getNestedObject(linkedContent, ['_links', 'site', 'href']); 45 | internalLink = getNestedObject(linkedContent, ['_links', 'site', 'type']); 46 | } 47 | break; 48 | 49 | case 'href': 50 | href = link; 51 | internalLink = 'internal'; 52 | break; 53 | } 54 | 55 | // linkText is a function insteaf of a string, so that additional HTML can be included inside the anchor tag 56 | if (href && internalLink && typeof linkText === 'function') { 57 | return internalLink === 'internal' && typeof externalCreateLinkFunction === 'function' 58 | ? externalCreateLinkFunction(href, linkText, className) 59 | : {linkText()}; 60 | } 61 | 62 | return null; 63 | } 64 | 65 | export default function createLink(linkType, link, linkText, className) { 66 | return ( 67 | 68 | { (pageModel) => 69 | { (externalCreateLinkFunction) => _createLink( 70 | linkType, 71 | link, 72 | linkText, 73 | className, 74 | externalCreateLinkFunction, 75 | pageModel, 76 | ) } 77 | } 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/content-component-wrapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import jsonpointer from 'jsonpointer'; 19 | import PlaceholderComponent from './placeholder'; 20 | import UndefinedComponent from './undefined'; 21 | import CmsEditButton from './cms-edit-button'; 22 | import { ComponentDefinitionsContext } from '../../context'; 23 | import getNestedObject from '../../utils/get-nested-object'; 24 | 25 | export default class ContentComponentWrapper extends React.Component { 26 | renderContentComponentWrapper(component, pageModel, content, preview, componentDefinitions, manageContentButton) { 27 | // based on the type of the component, render a different React component 28 | if (component.label in componentDefinitions && componentDefinitions[component.label].component) { 29 | return React.createElement(componentDefinitions[component.label].component, { 30 | content, pageModel, preview, manageContentButton, 31 | }, null); 32 | } 33 | 34 | return ; 35 | } 36 | 37 | render() { 38 | const { configuration, pageModel, preview } = this.props; 39 | 40 | // get content from model 41 | let contentRef = getNestedObject(configuration, ['models', 'document', '$ref']); 42 | let content; 43 | if (!contentRef) { 44 | // NewsList component passed document ID through property instead of via reference in attributes map 45 | ({ contentRef } = this.props); 46 | } 47 | 48 | if (contentRef && (typeof contentRef === 'string' || contentRef instanceof String)) { 49 | content = jsonpointer.get(pageModel, contentRef); 50 | } 51 | 52 | if (!content && preview) { 53 | // return placeholder if no document is set on component 54 | return ; 55 | } 56 | 57 | if (!content) { 58 | // don't render placeholder outside of preview mode 59 | return null; 60 | } 61 | 62 | // create edit content button and pass as a prop 63 | const manageContentButton = preview ? : null; 64 | 65 | return ( 66 | 67 | { (componentDefinitions) => this.renderContentComponentWrapper( 68 | configuration, 69 | pageModel, 70 | content, 71 | preview, 72 | componentDefinitions, 73 | manageContentButton, 74 | ) } 75 | 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/client-side-rendered/src/components/news-list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { ContentComponentWrapper, getNestedObject, Placeholder } from 'bloomreach-experience-react-sdk'; 19 | 20 | export function NewsListPagination(props) { 21 | if (!props.showPagination) { 22 | return null; 23 | } 24 | 25 | return ( 26 | 47 | ); 48 | } 49 | 50 | export default class NewsList extends React.Component { 51 | render() { 52 | const { preview, pageModel, configuration } = this.props; 53 | 54 | // return placeholder if no list is set on component 55 | let list = getNestedObject(configuration, ['models', 'pageable', 'items', 0]); 56 | if (!list) { 57 | return preview 58 | ? 59 | : null; 60 | } 61 | 62 | list = configuration.models.pageable.items; 63 | 64 | // build list of news articles 65 | const listItems = list.map((listItem, index) => { 66 | if (!configuration 67 | || typeof configuration !== 'object' 68 | || configuration.constructor !== Object 69 | || !('$ref' in listItem)) { 70 | console.log('NewsList component configuration is not a map, unexpected format of configuration'); 71 | return null; 72 | } 73 | 74 | const newsItemConfig = { label: 'News Item' }; 75 | 76 | return ; 83 | }); 84 | 85 | return ( 86 |
    87 |
    88 | {listItems} 89 |
    90 |
    91 | 92 |
    93 |
    94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/components/news-list.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { ContentComponentWrapper, getNestedObject, Placeholder } from 'bloomreach-experience-react-sdk'; 19 | 20 | export function NewsListPagination(props) { 21 | if (!props.showPagination) { 22 | return null; 23 | } 24 | 25 | return ( 26 | 47 | ); 48 | } 49 | 50 | export default class NewsList extends React.Component { 51 | render() { 52 | const { preview, pageModel, configuration } = this.props; 53 | 54 | // return placeholder if no list is set on component 55 | let list = getNestedObject(configuration, ['models', 'pageable', 'items', 0]); 56 | if (!list) { 57 | return preview 58 | ? 59 | : null; 60 | } 61 | 62 | list = configuration.models.pageable.items; 63 | 64 | // build list of news articles 65 | const listItems = list.map((listItem, index) => { 66 | if (!configuration 67 | || typeof configuration !== 'object' 68 | || configuration.constructor !== Object 69 | || !('$ref' in listItem)) { 70 | console.log('NewsList component configuration is not a map, unexpected format of configuration'); 71 | return null; 72 | } 73 | 74 | const newsItemConfig = { label: 'News Item' }; 75 | 76 | return ; 83 | }); 84 | 85 | return ( 86 |
    87 |
    88 | {listItems} 89 |
    90 |
    91 | 92 |
    93 |
    94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/container-item.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import ContentComponentWrapper from './content-component-wrapper'; 19 | import UndefinedComponent from './undefined'; 20 | import { addBeginComment, addEndComment } from '../../utils/add-html-comment'; 21 | import { ComponentDefinitionsContext, PageModelContext, PreviewContext } from '../../context'; 22 | 23 | export default class CmsContainerItem extends React.Component { 24 | renderContainerItem(configuration, pageModel, preview, componentDefinitions) { 25 | if (preview && configuration) { 26 | return ( 27 |
    { this.addMetaData(containerItemElm, configuration, preview); }}> 29 | { this.renderContainerItemComponent(configuration, pageModel, preview, componentDefinitions) } 30 |
    31 | ); 32 | } 33 | 34 | if (configuration) { 35 | return this.renderContainerItemComponent(configuration, pageModel, preview, componentDefinitions); 36 | } 37 | 38 | return null; 39 | } 40 | 41 | renderContainerItemComponent(component, pageModel, preview, componentDefinitions) { 42 | // component not defined in component-definitions 43 | if (!(component.label in componentDefinitions)) { 44 | return ; 45 | } 46 | 47 | const componentDefinition = componentDefinitions[component.label]; 48 | // based on the type of the component, render a different React component 49 | if ('wrapInContentComponent' in componentDefinition 50 | && componentDefinition.wrapInContentComponent) { 51 | // wrap component in ContentComponentWrapper class 52 | return ; 58 | } 59 | 60 | if (componentDefinition.component) { 61 | // component is defined and does not have to be wrapped in ContentComponent, so render the actual component 62 | return React.createElement(componentDefinition.component, { 63 | configuration: component, 64 | pageModel, 65 | preview, 66 | componentDefinitions, 67 | }, null); 68 | } 69 | 70 | return null; 71 | } 72 | 73 | addMetaData(htmlElm, configuration, preview) { 74 | addBeginComment(htmlElm, 'afterbegin', configuration, preview); 75 | addEndComment(htmlElm, 'beforeend', configuration, preview); 76 | } 77 | 78 | render() { 79 | const { configuration } = this.props; 80 | 81 | return ( 82 | 83 | { (pageModel) => 84 | { (preview) => 85 | { (componentDefinitions) => this.renderContainerItem( 86 | configuration, 87 | pageModel, 88 | preview, 89 | componentDefinitions, 90 | ) } 91 | } 92 | } 93 | 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /examples/client-side-rendered/src/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import ReactDOM from 'react-dom'; 19 | import { 20 | BrowserRouter, 21 | Link, 22 | Redirect, 23 | Route, 24 | Switch, 25 | } from 'react-router-dom'; 26 | import { CmsPage, RenderCmsComponent } from 'bloomreach-experience-react-sdk'; 27 | 28 | import Banner from './components/banner'; 29 | import Content from './components/content'; 30 | import CmsMenu from './components/menu'; 31 | import NewsItem from './components/news-item'; 32 | import NewsList from './components/news-list'; 33 | 34 | const BR_ORIGIN = new URL(process.env.REACT_APP_BR_ORIGIN); 35 | const BR_CONTEXT_PATH = process.env.REACT_APP_BR_CONTEXT_PATH; 36 | const BR_CHANNEL_PATH = process.env.REACT_APP_BR_CHANNEL_PATH; 37 | 38 | const urlConfig = { 39 | scheme: BR_ORIGIN.protocol.slice(0, -1), 40 | hostname: BR_ORIGIN.hostname, 41 | port: BR_ORIGIN.port, 42 | contextPath: BR_CONTEXT_PATH, 43 | channelPath: BR_CHANNEL_PATH 44 | }; 45 | 46 | const cmsUrls = { 47 | preview: urlConfig, 48 | live: urlConfig 49 | }; 50 | 51 | const componentDefinitions = { 52 | Banner: { component: Banner, wrapInContentComponent: true }, 53 | Content: { component: Content, wrapInContentComponent: true }, 54 | 'News List': { component: NewsList }, 55 | 'News Item': { component: NewsItem, wrapInContentComponent: true }, 56 | }; 57 | 58 | const createLink = (href, linkText, className) => {linkText()}; 59 | 60 | class App extends React.Component { 61 | render() { 62 | // hostname and URL-path are used for detecting if site is viewed in CMS preview 63 | // and for fetching Page Model for the viewed page 64 | const request = { hostname: window.location.hostname, path: window.location.pathname + window.location.search }; 65 | 66 | return ( 67 | 68 | 85 |
    86 | 87 |
    88 |
    89 | ); 90 | } 91 | } 92 | 93 | ReactDOM.render( 94 | 95 | 96 | 97 | 98 | 99 | , 100 | document.getElementById('root'), 101 | ); 102 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/container.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import CmsContainerItem from './container-item'; 19 | import { addBeginComment, addEndComment } from '../../utils/add-html-comment'; 20 | import { ComponentDefinitionsContext, PageModelContext, PreviewContext } from '../../context'; 21 | 22 | export default class CmsContainer extends React.Component { 23 | renderContainerWrapper(configuration, pageModel, preview, componentDefinitions) { 24 | if (preview) { 25 | return ( 26 | // need to wrap container inside a div instead of React.Fragment because otherwise HTML comments are not removed 27 |
    28 |
    { this.addMetaData(containerElm, configuration, preview); }}> 30 | { this.renderContainer(configuration, pageModel, preview, componentDefinitions) } 31 |
    32 |
    33 | ); 34 | } 35 | 36 | return this.renderContainer(configuration, pageModel, preview, componentDefinitions); 37 | } 38 | 39 | renderContainer(configuration = { components: [] }, pageModel, preview, componentDefinitions) { 40 | const { components, label } = configuration; 41 | 42 | // don't render anything when there're no components found 43 | if (!components || !components.length) { 44 | return null; 45 | } 46 | 47 | // get component item components 48 | const containerItemComponents = components.map((component) => ( 49 | 50 | )); 51 | 52 | // check if component container is found in component definitions 53 | const ContainerComponent = componentDefinitions[label] && componentDefinitions[label].component; 54 | 55 | // if found then wrap container items with this component 56 | if (!ContainerComponent) { 57 | return ( 58 | 59 | { containerItemComponents } 60 | 61 | ); 62 | } 63 | 64 | return React.createElement( 65 | ContainerComponent, 66 | { 67 | configuration, 68 | pageModel, 69 | preview, 70 | componentDefinitions, 71 | }, 72 | containerItemComponents, 73 | ); 74 | } 75 | 76 | addMetaData(htmlElm, configuration, preview) { 77 | addBeginComment(htmlElm, 'beforebegin', configuration, preview); 78 | addEndComment(htmlElm, 'afterend', configuration, preview); 79 | } 80 | 81 | render() { 82 | if (!this.props.configuration) { 83 | return null; 84 | } 85 | 86 | return ( 87 | 88 | {(pageModel) => ( 89 | 90 | {(preview) => ( 91 | 92 | {(componentDefinitions) => this.renderContainerWrapper( 93 | this.props.configuration, 94 | pageModel, 95 | preview, 96 | componentDefinitions, 97 | )} 98 | 99 | )} 100 | 101 | )} 102 | 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /examples/server-side-rendered/src/pages/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import getConfig from 'next/config'; 19 | import Error from 'next/error'; 20 | import Link from 'next/link'; 21 | import { withRouter } from 'next/router'; 22 | 23 | import fetch from 'isomorphic-unfetch'; 24 | import { getApiUrl, CmsPage, RenderCmsComponent } from 'bloomreach-experience-react-sdk'; 25 | 26 | import Banner from '../components/banner'; 27 | import Content from '../components/content'; 28 | import CmsMenu from '../components/menu'; 29 | import NewsItem from '../components/news-item'; 30 | import NewsList from '../components/news-list'; 31 | 32 | const { publicRuntimeConfig } = getConfig(); 33 | const brOrigin = new URL(publicRuntimeConfig.brOrigin); 34 | 35 | const urlConfig = { 36 | scheme: brOrigin.protocol.slice(0, -1), 37 | hostname: brOrigin.hostname, 38 | port: brOrigin.port, 39 | contextPath: publicRuntimeConfig.brContextPath, 40 | channelPath: publicRuntimeConfig.brChannelPath, 41 | }; 42 | 43 | const cmsUrls = { 44 | preview: urlConfig, 45 | live: urlConfig, 46 | }; 47 | 48 | const componentDefinitions = { 49 | Banner: { component: Banner, wrapInContentComponent: true }, 50 | Content: { component: Content, wrapInContentComponent: true }, 51 | 'News List': { component: NewsList }, 52 | 'News Item': { component: NewsItem, wrapInContentComponent: true }, 53 | }; 54 | 55 | const createLink = (href, linkText, className) => {linkText()}; 56 | 57 | export class Index extends React.Component { 58 | static async getInitialProps({ req, asPath }) { 59 | // setting pageModel to empty list instead of null value, 60 | // as otherwise the API will be fetched client-side again after server-side fetch errors 61 | let pageModel = {}; 62 | 63 | // hostname and URL-path are used for detecting if site is viewed in CMS preview 64 | // and for fetching Page Model for the viewed page 65 | const request = { 66 | hostname: req.headers.host, 67 | path: asPath, 68 | }; 69 | const url = getApiUrl(request, cmsUrls); 70 | const response = await fetch(url, { headers: { Cookie: req.headers.cookie } }); 71 | 72 | if (response.ok) { 73 | try { 74 | pageModel = await response.json(); 75 | } catch (err) { 76 | console.log(`Error! Could not convert response to JSON for URL: ${url}`); 77 | console.log(err); 78 | } 79 | } else { 80 | console.log(`Error! Status code ${response.status} while fetching CMS page data for URL: ${url}`); 81 | } 82 | 83 | return { 84 | pageModel, 85 | request, 86 | errorCode: !response.ok ? response.status : null, 87 | }; 88 | } 89 | 90 | render() { 91 | const { errorCode, request } = this.props; 92 | 93 | if (errorCode) { 94 | return (); 95 | } 96 | 97 | return ( 98 | 100 | 112 |
    113 | 114 |
    115 |
    116 | ); 117 | } 118 | } 119 | 120 | export default withRouter(Index); 121 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/cms-urls.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import pathToRegexp from 'path-to-regexp'; 18 | 19 | export const FULLY_QUALIFIED_LINK = /\w+:\/\//; 20 | 21 | const defaultCmsUrls = { 22 | scheme: 'http', 23 | hostname: 'localhost', 24 | port: '8080', 25 | contextPath: 'site', 26 | channelPath: '', 27 | previewPrefix: '_cmsinternal', 28 | apiPath: 'resourceapi', 29 | apiComponentRenderingUrlSuffix: '?_hn:type=component-rendering&_hn:ref=', 30 | }; 31 | 32 | function setUrlsWithDefault(urls = {}, defaultUrls = {}) { 33 | const newUrls = { 34 | scheme: urls.scheme || defaultUrls.scheme, 35 | hostname: urls.hostname || defaultUrls.hostname, 36 | port: urls.port !== undefined ? urls.port : defaultUrls.port, 37 | contextPath: urls.contextPath !== undefined ? urls.contextPath : defaultUrls.contextPath, 38 | channelPath: urls.channelPath || defaultUrls.channelPath, 39 | previewPrefix: urls.previewPrefix !== undefined ? urls.previewPrefix : defaultUrls.previewPrefix, 40 | apiPath: urls.apiPath || defaultUrls.apiPath, 41 | apiComponentRenderingUrlSuffix: urls.apiComponentRenderingUrlSuffix || defaultUrls.apiComponentRenderingUrlSuffix, 42 | }; 43 | 44 | newUrls.baseUrl = `${newUrls.scheme}://${newUrls.hostname}`; 45 | if (newUrls.port) { 46 | newUrls.baseUrl = `${newUrls.baseUrl}:${newUrls.port}`; 47 | } 48 | 49 | return newUrls; 50 | } 51 | 52 | const cmsUrls = {}; 53 | 54 | export function updateCmsUrls(urls = {}) { 55 | if (typeof urls !== 'object') { 56 | console.log('Warning! Supplied CMS URLs not of type object. Using default URLs.'); 57 | urls = {}; 58 | } 59 | 60 | cmsUrls.live = setUrlsWithDefault(urls.live, defaultCmsUrls); 61 | cmsUrls.preview = setUrlsWithDefault(urls.preview, cmsUrls.live); 62 | 63 | const pathregexp = `${cmsUrls.live.contextPath !== '' ? `/:contextPath(${cmsUrls.live.contextPath})?` : ''}` 64 | + `/:previewPrefix(${cmsUrls.live.previewPrefix})?` 65 | + `${cmsUrls.live.channelPath !== '' ? `/:channelPath(${cmsUrls.live.channelPath})?` : ''}` 66 | + '/:pathInfo*'; 67 | 68 | cmsUrls.regexpKeys = []; 69 | cmsUrls.regexp = pathToRegexp(pathregexp, cmsUrls.regexpKeys); 70 | 71 | return cmsUrls; 72 | } 73 | 74 | export function buildApiUrl(pathInfo, query, preview, componentId, urls) { 75 | // when using fetch outside of CmsPage for SSR, cmsUrls need to be supplied 76 | if (!urls) { 77 | urls = cmsUrls; 78 | } 79 | urls = urls[preview ? 'preview' : 'live']; 80 | 81 | let url = urls.baseUrl; 82 | // add api path to URL, and prefix with contextPath and preview-prefix if used 83 | if (urls.contextPath !== '') { 84 | url += `/${urls.contextPath}`; 85 | } 86 | if (preview && urls.previewPrefix !== '') { 87 | url += `/${urls.previewPrefix}`; 88 | } 89 | if (urls.channelPath !== '') { 90 | url += `/${urls.channelPath}`; 91 | } 92 | url += `/${urls.apiPath}`; 93 | if (pathInfo) { 94 | url += `/${pathInfo}`; 95 | } 96 | // if component ID is supplied, URL should be a component rendering URL 97 | if (componentId) { 98 | url += urls.apiComponentRenderingUrlSuffix + componentId; 99 | } 100 | if (query) { 101 | url += (url.includes('?') ? '&' : '?') + query; 102 | } 103 | 104 | return url; 105 | } 106 | 107 | function hasPreviewQueryParameter(query) { 108 | return query.startsWith('bloomreach-preview=true') 109 | || query.indexOf('&bloomreach-preview=true') !== -1; 110 | } 111 | 112 | // if hostname is different for preview and live, 113 | // then hostname can be used to detect if we're in preview mode 114 | function isMatchingPreviewHostname(hostname, urls) { 115 | return urls.live.hostname !== urls.preview.hostname 116 | && hostname === urls.preview.hostname; 117 | } 118 | 119 | export function parseRequest(request = {}, urls) { 120 | if (!urls) { 121 | urls = cmsUrls; 122 | } 123 | 124 | const [urlPath, query = ''] = request.path.split('?', 2); 125 | const [hostname] = request.hostname.split(':', 2); 126 | const results = urls.regexp.exec(urlPath); 127 | let preview = hasPreviewQueryParameter(query) || isMatchingPreviewHostname(hostname, urls); 128 | if (!preview && results) { 129 | const previewIdx = urls.regexpKeys.findIndex((obj) => obj.name === 'previewPrefix'); 130 | preview = results[previewIdx + 1] !== undefined; 131 | } 132 | 133 | let path = ''; 134 | if (results) { 135 | const pathIdx = urls.regexpKeys.findIndex((obj) => obj.name === 'pathInfo'); 136 | // query parameter is not needed for fetching API URL and can actually conflict with component rendering URLs 137 | path = results[pathIdx + 1] || ''; 138 | } 139 | 140 | return { path, preview, query }; 141 | } 142 | 143 | export function getApiUrl(request, newCmsUrls = {}) { 144 | // eslint-disable-next-line no-shadow 145 | const cmsUrls = updateCmsUrls(newCmsUrls); 146 | const parsedRequest = parseRequest(request, cmsUrls); 147 | 148 | return buildApiUrl(parsedRequest.path, parsedRequest.query, parsedRequest.preview, null, cmsUrls); 149 | } 150 | 151 | updateCmsUrls(); 152 | 153 | export default cmsUrls; 154 | -------------------------------------------------------------------------------- /packages/sdk/src/cms-components/core/page.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Hippo B.V. (http://www.onehippo.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import React from 'react'; 18 | import { 19 | ComponentDefinitionsContext, 20 | CreateLinkContext, 21 | PageModelContext, 22 | PreviewContext, 23 | } from '../../context'; 24 | import { addBodyComments } from '../../utils/add-html-comment'; 25 | import { updateCmsUrls, parseRequest } from '../../utils/cms-urls'; 26 | import { fetchCmsPage, fetchComponentUpdate } from '../../utils/fetch'; 27 | import findChildById from '../../utils/find-child-by-id'; 28 | 29 | export default class CmsPage extends React.Component { 30 | constructor(props) { 31 | super(props); 32 | updateCmsUrls(this.props.cmsUrls); 33 | 34 | this.state = parseRequest(this.props.request); 35 | this.state.createLink = this.props.createLink; 36 | if (typeof this.props.componentDefinitions === 'object') { 37 | this.state.componentDefinitions = this.props.componentDefinitions; 38 | } 39 | 40 | if (this.props.pageModel) { 41 | this.state.pageModel = this.props.pageModel; 42 | } 43 | } 44 | 45 | async fetchPageModel(path, query, preview) { 46 | if (this.props.debug) { 47 | console.log('### React SDK debugging ### fetching page model for URL-path \'%s\'', path); 48 | } 49 | const data = await fetchCmsPage(path, query, preview); 50 | this.updatePageModel(data); 51 | } 52 | 53 | updatePageModel(pageModel) { 54 | if (pageModel) { 55 | addBodyComments(pageModel.page, this.state.preview); 56 | } 57 | 58 | this.setState({ 59 | pageModel, 60 | }); 61 | if (this.state.preview && this.cms && typeof this.cms.createOverlay === 'function') { 62 | if (this.props.debug) { 63 | console.log('### React SDK debugging ### creating CMS overlay'); 64 | } 65 | this.cms.createOverlay(); 66 | } 67 | } 68 | 69 | initializeCmsIntegration() { 70 | if (!this.state.preview || typeof window === 'undefined') { 71 | return; 72 | } 73 | 74 | window.SPA = { 75 | renderComponent: this.updateComponent.bind(this), 76 | init: (cms) => { 77 | this.cms = cms; 78 | if (this.state.pageModel) { 79 | if (this.props.debug) { 80 | console.log('### React SDK debugging ### creating CMS overlay'); 81 | } 82 | cms.createOverlay(); 83 | } 84 | }, 85 | }; 86 | } 87 | 88 | async updateComponent(componentId, propertiesMap) { 89 | if (this.props.debug) { 90 | console.log('### React SDK debugging ### component update triggered for \'%s\' with properties:', componentId); 91 | console.dir(propertiesMap); 92 | } 93 | // find the component that needs to be updated in the page structure object using its ID 94 | const componentToUpdate = findChildById(this.state.pageModel, componentId); 95 | if (componentToUpdate == null) { 96 | return; 97 | } 98 | 99 | const response = await fetchComponentUpdate( 100 | this.state.path, 101 | this.state.query, 102 | this.state.preview, 103 | componentId, 104 | propertiesMap, 105 | ); 106 | // API can return empty response when component is deleted 107 | if (!response) { 108 | return; 109 | } 110 | 111 | if (response.page) { 112 | componentToUpdate.parent[componentToUpdate.idx] = response.page; 113 | } 114 | 115 | const pageModel = { ...this.state.pageModel }; 116 | if (response.content) { 117 | pageModel.content = { ...pageModel.content, ...response.content }; 118 | } 119 | 120 | this.setState({ pageModel }); 121 | } 122 | 123 | componentDidUpdate(prevProps, prevState) { 124 | if (this.props.pageModel !== prevProps.pageModel) { 125 | this.updatePageModel(this.props.pageModel); 126 | } else if (this.props.request.path !== prevProps.request.path) { 127 | const parsedUrl = parseRequest(this.props.request); 128 | this.fetchPageModel(parsedUrl.path, parsedUrl.query, parsedUrl.preview); 129 | } 130 | 131 | if (this.state.pageModel !== prevState.pageModel && this.cms) { 132 | this.cms.createOverlay(); 133 | } 134 | } 135 | 136 | componentDidMount() { 137 | this.initializeCmsIntegration(); 138 | // fetch page model if not supplied 139 | if (!this.state.pageModel) { 140 | this.fetchPageModel(this.state.path, this.state.query, this.state.preview); 141 | } else { 142 | // add body comments client-side as document variable is undefined server-side 143 | addBodyComments(this.state.pageModel.page, this.state.preview); 144 | } 145 | } 146 | 147 | render() { 148 | const { pageModel } = this.state; 149 | 150 | if (!pageModel || !pageModel.page) { 151 | return null; 152 | } 153 | 154 | return ( 155 | 156 | 157 | 158 | 159 | { typeof this.props.children === 'function' ? this.props.children() : this.props.children } 160 | 161 | 162 | 163 | 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /packages/sdk/README.md: -------------------------------------------------------------------------------- 1 | # BloomReach Experience SDK for React 2 | 3 | > The BloomReach Experience SDK for React works only with [Bloomreach Experience](https://www.bloomreach.com/en/products/experience-manager) version 13 and below. Please use [@bloomreach/react-sdk](https://www.npmjs.com/package/@bloomreach/react-sdk) with all the latest versions of Bloomreach Experience. 4 | 5 | SDK for powering content and components in React applications by [BloomReach Experience](https://www.bloomreach.com/en/products/experience). 6 | This library makes integrating a React app with BloomReach Experience a breeze. It supports both 7 | client-side- and server-side rendered/isomorphic React apps. 8 | 9 | BloomReach Experience allows you to use an external front-end such as React for rendering while still 10 | providing a native-like authoring experience, such as integrated preview, in-context editing, drag & 11 | drop, server-side personalization. For more information on this approach, see [A new approach to 12 | integrating SPA's with WCM: Fixing what's wrong with headless integrations](https://www.bloomreach.com/en/blog/2018/03/a-new-approach-to-integrating-spas-with-wcm-fixing-what%E2%80%99s-wrong-with-headless-integrations.html). 13 | 14 | ## Install 15 | 16 | ```bash 17 | npm install --save bloomreach-experience-react-sdk 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```jsx 23 | import React from 'react' 24 | import { CmsPage, RenderCmsComponent } from 'bloomreach-experience-react-sdk' 25 | 26 | const componentDefinitions = { 27 | "MyCustomComponent": { component: MyCustomComponent } 28 | } 29 | 30 | class MyApp extends React.Component { 31 | render() { 32 | const request = { hostname: window.location.hostname, path: window.location.pathname + window.location.search }; 33 | return ( 34 | 35 | 36 | 37 | ) 38 | } 39 | } 40 | ``` 41 | 42 | At a minimum, `` and `` should be imported in the React app. 43 | 44 | `` fetches the Page Model when it is not supplied as a prop (see section on server-side 45 | rendering); it fetches updates to the page model on component changes in the CMS; and it initializes and 46 | manages state, and provides this as context to ``. The `` component can be 47 | put anywhere within the React App. 48 | 49 | `` renders components based on the Page Model API response. It consumes the state 50 | from `` and renders the container components and any content referenced from the components as 51 | contained in the Page Model API response. The `` should be nested within `` 52 | and placed in the React app at the exact location where you want BloomReach Experience to control what 53 | components and content is rendered. 54 | 55 | `` takes two props: `componentDefinitions` and `request`. 56 | 57 | The `request` prop is used to fetch the Page Model for the current page; and to detect whether 58 | preview mode is active so that meta-data for Channel Manager functionality (e.g. in-context editing) is 59 | included in the HTML, and consequently Channel Manager functionality is enabled. 60 | 61 | Finally, component definitions are supplied through the `componentDefinitions` prop. The component 62 | definitions tell `` what React component to render by mapping these to the 63 | *hst:label* of a CMS catalog component. 64 | 65 | ## Server-side rendering 66 | 67 | For server-side rendering (e.g. using [Next.js](https://github.com/zeit/next.js)) you need to fetch the 68 | Page Model API server-side and supply it as a prop to ``. Apart from this the usage is the same 69 | as with client-side rendering. 70 | 71 | The helper function `getApiUrl()` can be used to generate the Page Model API URL using the current 72 | request. 73 | 74 | It is important to pass cookies from the initial request and supply these with the request header of the 75 | Page Model API request. Otherwise you will get a 403 error in preview mode. 76 | 77 | Finally, to get preview to work for server-side rendered apps you can use a reverse proxy to route 78 | requests from BloomReach's site preview to the React App-server. For more details, see section *3.2 79 | Server-side apps* of the Hippo Lab: [Integrate a React application with BloomReach Experience](https://www.onehippo.org/labs/integrate-a-react-application-with-bloomreach-experience.html). 80 | 81 | ```jsx 82 | import fetch from 'isomorphic-unfetch' 83 | import { getApiUrl, CmsPage, RenderCmsComponent } from 'bloomreach-experience-react-sdk' 84 | 85 | // setting pageModel to empty list instead of null value, 86 | // as otherwise the API will be fetched client-side again after server-side fetch errors 87 | let pageModel = {} 88 | 89 | const request = { hostname: req.headers.host, path: asPath } 90 | const url = getApiUrl(request, {}) 91 | // pass cookies of initial request for CMS preview 92 | const response = await fetch(url, {headers: {'Cookie': req.headers.cookie}}) 93 | 94 | if (response.ok) { 95 | try { 96 | pageModel = await response.json() 97 | } catch (err) { 98 | console.log(`Error! Could not convert response to JSON for URL: ${url}`) 99 | } 100 | } else { 101 | console.log(`Error! Status code ${response.status} while fetching CMS page data for URL: ${url}`) 102 | } 103 | ``` 104 | 105 | ## Example React apps 106 | 107 | There are two example React apps available that use the SDK. One client-side rendered, and one 108 | server-side rendered using [Next.js](https://github.com/zeit/next.js). You can find the [example apps on 109 | Github](https://github.com/bloomreach/experience-react-sdk/tree/master/examples). 110 | 111 | ## Creating custom components 112 | 113 | Any components that are supplied through the `compenentDefinitions` prop can be any type of valid React 114 | component, including functional components. Props are passed to these components as context so that you 115 | can access the components' configuration, content, etc. See more information below. 116 | 117 | ### Props 118 | 119 | The following props are passed to a component that is rendered by ``: 120 | - `configuration` - `Object` component configuration. Contains the contributed models, raw parameters 121 | and resolved parameters. Content included in the component's model is not serialized as part of the 122 | component's configuration but in a separate content object, and a JSON Pointer reference is used to 123 | link to the actual content object. This is done to prevent content from being included multiple times 124 | in the API response when referenced multiple times on a page. 125 | - `pageModel` - `Object` full Page Model API response. 126 | - `preview` - `Boolean` is *true* if preview mode is active based on current URL supplied through 127 | `request` prop of ``. 128 | 129 | For more information on the Page Model API response, see the [Swagger documentation](https://www.onehippo.org/library/concepts/spa-plus/page-model-api/swagger-api-documentation.html). 130 | 131 | ### Example 132 | 133 | ```jsx 134 | import React from 'react' 135 | import NewsItem from './news-item' 136 | import { getNestedObject } from 'bloomreach-experience-react-sdk' 137 | 138 | export default class NewsList extends React.Component { 139 | render() { 140 | const preview = this.props.preview 141 | const pageModel = this.props.pageModel 142 | const configuration = this.props.configuration 143 | 144 | // return placeholder if no list is set on component 145 | const list = getNestedObject(configuration, ['models', 'pageable', 'items']) 146 | if (!list && preview) { 147 | return (

    Click to configure {configuration.label}

    ) 148 | } else if (!list) { 149 | // don't render placeholder outside of preview mode 150 | return null 151 | } 152 | 153 | // build list of news articles 154 | const listItems = list.map((listItem, index) => { 155 | if ('$ref' in listItem) { 156 | return () 157 | } 158 | }) 159 | 160 | return ( 161 |
    162 |
    163 | {listItems} 164 |
    165 |
    166 | ) 167 | } 168 | } 169 | ``` 170 | 171 | ### Content components 172 | 173 | Components that reference a single content-item (e.g. the Banner component) can use a convenient 174 | wrapper-class that looks up the content and passes it as a prop. See below. 175 | 176 | To enable this, the property `wrapInContentComponent: true` has to be set on the component in the 177 | `componentDefinitions` prop. See `componentDefinitions` in the API section for more details. 178 | 179 | #### Props 180 | 181 | - `content` - `Object` raw content object that contains the content-item's fields and field-values. Any 182 | references to other content-items (e.g. images) are serialized as JSON Pointers. 183 | - `manageContentButton` - `React.Component` for placement and rendering of the [Manage Content Button](https://www.onehippo.org/library/concepts/component-development/render-manage-content-button.html) 184 | in preview mode in CMS. 185 | - `pageModel` - `Object` full Page Model API response. 186 | - `preview` - `Boolean` is *true* if preview is active based on current URL supplied through `request` 187 | prop of ``. 188 | 189 | #### Example 190 | 191 | ```jsx 192 | import React from 'react' 193 | 194 | const content = this.props.content; 195 | const manageContentButton = this.props.manageContentButton; 196 | 197 | class Banner extends React.Component { 198 | render() { 199 | return ( 200 |
    201 | { manageContentButton && manageContentButton } 202 | { content.title && content.title } 203 |
    ) 204 | } 205 | } 206 | ``` 207 | 208 | ### Static CMS components 209 | 210 | Static CMS components are components that are defined by developers / administrators and cannot be 211 | modified by users in the CMS. However, any content or site menus these components reference can be 212 | changed by users in the CMS. 213 | 214 | Since `` only renders container components (drag-and-drop components) by default, 215 | you have to specify two additional properties in order to render a static CMS component: the `path` 216 | property to point to the relative path of the component, and `renderComponent` to specify which React 217 | component to use for rendering the component. See the example below. 218 | 219 | ```jsx 220 | 221 | ``` 222 | 223 | ### Containers 224 | 225 | Containers are being used to hold container items, which will be rendered by the SDK. Whenever it needs to customize a layout of those container items, it is possible also to pass a custom container component in `componentDefinitions`. 226 | 227 | #### Example 228 | ```jsx 229 | import React from 'react' 230 | import { CmsPage, RenderCmsComponent } from 'bloomreach-experience-react-sdk' 231 | 232 | function MyCustomFooter(props) { 233 | return ( 234 |
    235 | 236 | { props.children } 237 | 238 |
    239 | ); 240 | } 241 | 242 | const componentDefinitions = { 243 | "Footer Container": { component: MyCustomFooter } 244 | } 245 | 246 | // ... 247 | 248 | class MyApp extends React.Component { 249 | render() { 250 | return ( 251 | 252 | 253 | 254 | ) 255 | } 256 | } 257 | ``` 258 | 259 | ### More component examples 260 | 261 | For more detailed examples, see the components included in the [example applications](https://github.com/bloomreach/experience-react-sdk/tree/master/examples/client-side-rendered/src/components). 262 | 263 | ### Helper functions 264 | 265 | Additionally, there are a variety of helper functions available. See the examples below. For full 266 | details on the APIs, see the API section. 267 | 268 | ```jsx 269 | import { createLink, getImageUrl, getNestedObject, parseAndRewriteLinks, parseDate } from 'bloomreach-experience-react-sdk' 270 | 271 | const link = createLink('ref', link, linkText, className) 272 | const image = getImageUrl(content.image, this.props.pageModel, this.props.preview) 273 | const list = getNestedObject(configuration, ['models', 'pageable', 'items', 0]) 274 | const contentHtml = parseAndRewriteLinks(content.content.value, this.props.preview) 275 | const date = parseDate(content.date) 276 | ``` 277 | 278 | ## API 279 | 280 | ### `` 281 | 282 | The CmsPage component is a higher-order component that takes care of: 283 | - Fetching the Page Model for client-side rendering, when not supplied as a prop. 284 | - Fetching Page Model updates on component changes in the CMS. 285 | - Initializing and managing state, and providing this as context to ``. 286 | 287 | #### Properties 288 | 289 | - `cmsUrls` - `Object` Override default CMS URL's. (Optional) 290 | - `componentDefinitions` - `object` Mapping of CMS catalog components to React components. Determines 291 | what component to render based from the Page Model. (Optional) 292 | - `createLink` - `Function` Called when creating internal links so that links can be constructed using 293 | the router library of the React app. (Optional) 294 | - `pageModel` - `Object` Supply Page Model as prop. Used for server-side-rendering where Page Model API 295 | is fetched server-side. When supplied, `CmsPage` will not fetch Page Model API on mount, only on 296 | component updates in CMS. (Optional) 297 | - `request` - `String` Current URL-path for determining if preview mode is active, and for fetching the 298 | Page Model for the page that is active. (Required) 299 | 300 | ###### `cmsUrls` property 301 | 302 | Property that allows you to override the default URL's for fetching the Page Model API. Typically you 303 | will only have to define `scheme`, `hostname`, `port`, and `contextPath`. Input object takes the 304 | following properties (all are optional): 305 | - `scheme` - `String` scheme (default: *http*) 306 | - `hostname` - `String` hostname (default: *localhost*) 307 | - `port` - `number` port number (default: *8080*) 308 | - `contextPath` - `String` site context-path (default: *site*) 309 | - `channelPath` - `String` path to the used channel, if channel is accessed through a subpath 310 | - `previewPrefix` - `String` preview-prefix used by CMS (default: *_cmsinternal*) 311 | - `apiPath` - `String` path to Page Model API as subpath (default: *resourceapi*) 312 | - `apiComponentRenderingUrlSuffix` - `String` (default: *?_hn:type=component-rendering&_hn:ref=*) 313 | 314 | ###### `componentDefinitions` property 315 | 316 | Maps CMS catalog components to React components. Expects as input an object with `hst:label` of the CMS 317 | components as keys and as value another object. The nested object has the mandatory property `component` 318 | who's value maps the CMS component to a React component. See the example below: 319 | 320 | ```js 321 | const componentDefinitions = { 322 | "MyCustomCmsComponent": { component: MyCustomReactComponent }, 323 | "AnotherCmsComponent": { component: AnotherReactComponent, wrapInContentComponent: true } 324 | }; 325 | ``` 326 | 327 | Additionally, the property `wrapInContentComponent: true` can be used for components that reference a 328 | single content-item. When this property is set on a component, it will be wrapped in a convenient 329 | wrapper class. See section *Content components*. 330 | 331 | ##### `createLink` property 332 | 333 | Called when creating internal links so that links can be constructed using the router library of the 334 | React app. 335 | 336 | Takes `Function` as input. The function should return valid JSX and have three parameters: 337 | - `href` - `String` href of link 338 | - `linkText` - `Function` contains the HTML that is wrapped inside the link. Is a function so that it 339 | can include HTML. 340 | - `className` - `String` classnames to add to the link element 341 | 342 | For example: 343 | ```jsx 344 | const createLink = (href, linkText, className) => { 345 | return ({linkText()}) 346 | } 347 | ``` 348 | 349 | ##### `pageModel` property 350 | 351 | Page Model can be supplied as a prop when using a server-side rendered / isomorphic React framework. 352 | When supplied, `` will not fetch the Page Model API client-side. 353 | 354 | ##### `request` property 355 | 356 | Takes `Object` as input with properties `hostname` and `path`, both of type `String`. The property 357 | `hostname` should contain the hostname for the current request (client-side this is 358 | window.location.hostname). The property `path` should contain the URL-path for the current request 359 | (client-side this is window.location.pathname); 360 | 361 | ### `` 362 | 363 | Renders a CMS component and all of its children using the Page Model supplied by ``. Will 364 | render the entire Page Model by default. 365 | 366 | #### Properties 367 | 368 | - `path` - `String` path to a component (static CMS component), container or container-item in the Page 369 | Model to render only that component and its children. If no path is supplied, entire Page Model will be 370 | rendered. 371 | - `renderComponent` - `React.Component` render a static CMS component using specified React component. 372 | Only works in combination with `path` property, which should specify path to the static CMS component. 373 | Site menus that are rendered this way can leverage the `` component for rendering edit 374 | buttons in the CMS. 375 | 376 | #### Example 377 | 378 | ```jsx 379 | 380 | ``` 381 | 382 | ### `` 383 | 384 | Inserts meta-data for either a content-item or site menu for placing an edit button in preview mode in 385 | the CMS. Content-items that are rendered by a content component using `wrapInContentComponent: true` do 386 | not need to use this component, but should use the `manageContentButton` prop that passes the meta-data. 387 | 388 | #### Properties 389 | 390 | - `configuration` - `Object` configuration of the site menu (not the component configuration containing 391 | the menu) or the content-item object which has the `_meta` object in its root. 392 | - `preview` - `Boolean` toggle to prevent edit buttons from being rendered outside of preview. 393 | 394 | ### `getApiUrl(request, [newCmsUrls])` 395 | 396 | Helper function for generating URL to the Page Model API for server-side fetching of the Page Model. 397 | 398 | #### Arguments 399 | 400 | - `request` - `Object` containing hostname and URL-path as properties `hostname` and `path` 401 | respectively. See the `request` property section above. 402 | - `newCmsUrls` - `Object` takes `cmsUrls` property as input to override default CMS URL's 403 | 404 | #### Return types 405 | 406 | `String` returns URL for fetching Page Model API 407 | 408 | ### `createLink(linkType, link, linkText, className)` 409 | 410 | Creates a link to either a component or content-item itself or a referenced content-item (can be a 411 | document, image or asset) and returns the link as JSX. 412 | 413 | #### Arguments 414 | 415 | - `linkType` - `String` type of link to create. Valid values are `self`, `ref` or `href`. 416 | - `self` - Create link to the component or content-item itself. E.g. for a news-item in a news 417 | overview. 418 | - `ref` - Create link to a referenced content-item. E.g. for a banner. 419 | - `href` - Used by `parseAndRewriteLinks()` to create links using a href only. 420 | - `link` - `Object` the component configuration or content-object that is linking to itself or 421 | referenced another content-item. 422 | - `linkText` - `JSX | Function` HTML to wrap inside the link. Is a function so that it can include HTML. 423 | - `className` - `String` classnames to add to the link element 424 | 425 | #### Return types 426 | 427 | `JSX` returns link as JSX object that can be included as a variable anywhere within the return section 428 | of a React component's render method. 429 | 430 | ### `getImageUrl(imageRef, pageModel, preview)` 431 | 432 | Creates link to URL of image in case BloomReach Experience is used for serving images. 433 | 434 | #### Arguments 435 | 436 | - `imageRef` - `String` JSON Pointer that references the image. 437 | - `pageModel` - `Object` since this function is a pure JavaScript function it can't get Page Model from 438 | context, so it has to be provided as function parameter. The Page Model is used to retrieve the image. 439 | - `preview` - `Boolean` toggle for whether preview mode is active. Components rendered by 440 | `` can pass the prop `this.props.preview`. 441 | 442 | #### Return types 443 | 444 | `String` returns URL to image. 445 | 446 | ### `getNestedObject(nestedObject, pathArray)` 447 | 448 | Returns a nested object or value using a path array. Useful when you need to access deeply nested 449 | objects/values without having to string null checks together. 450 | 451 | #### Arguments 452 | 453 | - `nestedObject` - `Object` the object containing the nested object or value. 454 | - `pathArray` - `Array` contains the path to the nested object as an array. 455 | 456 | #### Return types 457 | 458 | `Object|null` returns the nested object if found, otherwise returns null. 459 | 460 | ### `parseAndRewriteLinks(html, preview)` 461 | 462 | Parses HTML of a rich-text field of a content-item for rewriting links in HTML to internal links. Uses 463 | the `createLink` function passed to `` for constructing internal links. 464 | 465 | #### Arguments 466 | 467 | - `html` - `String` value of rich-text field of a content-item. Should contain HTML only. 468 | - `preview` - `Boolean` toggle for whether preview mode is active. Components rendered by 469 | `` can pass the prop `this.props.preview`. 470 | 471 | #### Return types 472 | 473 | `JSX` returns parsed and rewrited HTML as JSX. 474 | 475 | ### `parseDate(date)` 476 | 477 | Parses date-field of a content item and returns date as a string. 478 | 479 | #### Arguments 480 | 481 | - `date` - `String` takes raw value of a date-field as input, following the ISO 8601:2000 format. 482 | 483 | #### Return types 484 | 485 | `String` returns date in full date format. 486 | 487 | ## Release notes 488 | 489 | ### Version 0.6.4 490 | - Fix bug in the object type check in the `findChildById` function. 491 | 492 | ### Version 0.6.3 493 | - Add support of fully-qualified resource links. 494 | 495 | ### Version 0.6.2 496 | - Fixed page model fetch on the component update. 497 | 498 | ### Version 0.6.1 499 | - Fixed a bug with query string passing to the Page Model API. 500 | - Fixed a bug with missing key prop in the link rewriter. 501 | 502 | ### Version 0.6.0 503 | - Added eslint. 504 | - Migrated to rollup. 505 | - Migrated to yarn. 506 | - Fixed CmsPage children to not be wrapped around a function. 507 | - Added support of custom React components for container components. 508 | 509 | ### Version 0.5.2 510 | - Fixed bug with query string affecting on the path parsing. 511 | 512 | ### Version 0.5.1 513 | - Fixed bug with preview update on the component properties dialog changes; 514 | - Fixed bug with preview update on save/discard changes from the component properties dialog; 515 | - Fixed bug with the manage content button keeps referring to the old content after saving changes in the dialog. 516 | 517 | ### Version 0.5.0 518 | - Fixed bug with SSO handshake in client-side rendered applications. 519 | 520 | Upgrade steps: 521 | - Pass query string parameters along with other request details to ``: 522 | ```jsx 523 | const request = { hostname: window.location.hostname, path: window.location.path + window.location.search }; 524 | 525 | ``` 526 | 527 | ### Version 0.4.0 528 | 529 | Added support for rendering static CMS components. 530 | - Added new property `renderComponent` to `` which allows you to render a static 531 | component. This only works in combination with the `path` property, which should contain the relative 532 | path to the component. 533 | - Renamed `` to `` and exported it so it can be used by apps. This 534 | component is now more generic so it can also be used to generate edit buttons in the CMS for site menus. 535 | - Moved `` component out of SDK and into the example apps. 536 | 537 | 538 | ### Version 0.3.0 539 | 540 | Added extra checks so that `
    ` elements needed for CMS preview are only inserted in preview mode. 541 | 542 | ### Version 0.2.0 543 | 544 | This version includes significant changes. Please make sure to update your components using the upgrade 545 | steps further down. 546 | - Changed `cmsUrls` property to support different live and preview URLs. 547 | - Changed `urlPath` property to `request` property which no longer takes URL-path as string, but an 548 | object with hostname and URL-path. This property should be passed to ``. 549 | - Changed helper method `getApiUrl()` to take `request` property as parameter instead of `urlPath`. 550 | - Changed helper method `parseAndRewriteLinks()` to take an extra parameter: `preview`. 551 | - Changed helper method `getImageUrl()` to take an extra parameter: `preview`. 552 | 553 | Upgrade steps: 554 | - Modify `cmsUrls` property to include URLs for live and preview using the properties `live` and 555 | `preview` respectively. Please note that the `cms` prefix has been removed from all URL properties. So `cmsHostname` has become `hostname`. Preview URLs are optional. For example: 556 | ```js 557 | const cmsUrls = { 558 | live: { 559 | hostname: bloomreach.com 560 | }, 561 | preview: { 562 | hostname: cms.bloomreach.com 563 | } 564 | } 565 | ``` 566 | - Pass current request details through `request` property to ``. This was previously done 567 | through the `urlPath` property. The `request` property should be an object that contains the hostname 568 | and path as properties `hostname` and `path` respectively. For example: 569 | ```jsx 570 | const request = { hostname: window.location.hostname, path: window.location.path }; 571 | 572 | ``` 573 | - Update usage of `getApiUrl` to pass `request` property (see previous bullet) instead of URL-path. 574 | - Update usage of helper methods `getImageUrl()` and `parseAndRewriteLinks()` to include the preview 575 | parameter. Components rendered by `` have the preview value passed as prop 576 | `this.props.preview`. 577 | 578 | ## FAQ / Troubleshooting 579 | 580 | - Information about common problems and possible solutions can be found on [the troubleshooting page](https://documentation.bloomreach.com/library/concepts/spa-integration/troubleshooting.html). 581 | - Information about the recommended setup can be found on [the best practices page](https://documentation.bloomreach.com/library/concepts/spa-integration/best-practices.html). 582 | 583 | ## License 584 | 585 | Apache 2.0 586 | --------------------------------------------------------------------------------