This tutorial shows you how to create a Javascript application from scratch using React and GraphQL with Jahia as a headless content backend. The application displays a news list that is managed by Jahia’s headless content management UI: Content and Media Manager.
The tutorial shows how to create a standalone application that could be deployed in any website. If you’re interested in building a Javascript application hosted in a Jahia module and deployed on a Jahia server, we recommend you use a different starting point. We have provided a starter module project on Github at the following URL: https://github.com/Jahia/dx-react-starter
After completing the tutorial, you’ll be able to:
This tutorial is inspired by a live coding session that was presented at Jahia Days 2018. You can see the recording of this session here: https://www.youtube.com/watch?v=6fbkH-EIkcs
You will need the following tools to use the tutorial:
This tutorial uses the following technologies:
Web projects are virtual web sites that you can edit in Jahia. One single Jahia server can host several web projects for different teams and can handle separate domain names if needed.
To create a web project:
You use yarn create
to generate a skeleton React application by executing the following commands on the command line:
You will then see the URL that you can connect to using your browser to view the compiled application. You can then start performing changes that will be immediately visible in your browser, offering fast compile-deploy cycles.
Note that if some updates don’t work properly, just stop the server and restart it using yarn start
. That should fix most issues.
React Material is used to style the application’s look and feel using Google’s Material design components. This library provides an out-of-the-box React component library that makes building applications with a consistent look and feel much easier and faster.
To add React Material to the project:
yarn add @material-ui/core
<meta
name="viewport"
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
/>
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import CssBaseline from '@material-ui/core/CssBaseline';
import Grid from "@material-ui/core/Grid/Grid";
import Paper from "@material-ui/core/Paper/Paper";
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
}
export default App;
const App = () => {
return (<React.Fragment>
<CssBaseline/>
<AppBar position="static" color="default">
<Toolbar>
<Typography variant="h6" component="h1" color="inherit">
Companies
</Typography>
</Toolbar>
</AppBar>
<Grid container>
<Grid item xs={12}>
<Paper>
{/*CompanyList placeholder*/}
</Paper>
</Grid>
</Grid>
</React.Fragment>);
};
The AppBar displays at the top of the page with the Companies title to prepare the layout for the CompanyList component.
Before continuing, create the following folder structure in your project.
|____src
| |____components
| | |____Company
| | |____CompanyList
Following React best practices, this tutorial assumes that each component is created in a folder that identifies the component. For example, the CompanyList component is created under src/components/CompanyList. By doing this, you can take advantage of using an index.js file to export your component as the default.
Next, you will create the Company component that will be rendered in the CompanyList.
import React from 'react';
import {withStyles} from '@material-ui/core';
import CardMedia from "@material-ui/core/CardMedia";
import CardContent from "@material-ui/core/CardContent";
import Card from "@material-ui/core/Card";
import Typography from "@material-ui/core/Typography";
const styles = {
card: {
maxWidth: 300,
maxHeight: 350
},
media: {
height:'120px'
}
};
const Company = ({classes, title, description, image}) => {
return (
<Card className={classes.card}>
<CardMedia
className={classes.media}
image={image}
title="Company"
/>
<CardContent>
<Typography component="h1" variant="h4">
Company Name
</Typography>
<br/>
<Typography component="p">
{description.length > 150 ? `${description.substr(0,100)}...` : description}
</Typography>
</CardContent>
</Card>
);
};
export default withStyles(styles)(Company);
import Company from './Company';
export default Company;
Next, create a CompanyList component to retrieve the list of company objects from the Jahia server.
To create the CompanyList component:
import React from 'react';
import {withStyles} from '@material-ui/core';
import GridList from '@material-ui/core/GridList';
import GridListTile from '@material-ui/core/GridListTile';
import Company from "../Company";
const styles = theme => ({
root: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-around',
overflow: 'hidden',
backgroundColor: theme.palette.background.paper,
},
gridList: {
paddingTop:'12px',
width: 1020,
height:660
}
});
const CompanyList = ({classes}) => {
const title = 'Company name';
const description = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus a tortor hendrerit, dapibus libero eu, tincidunt nisl. Sed leo turpis, rutrum id condimentum quis, consequat eget enim. Nunc a tempor dui, eget tristique mi. Nunc ut ultrices sem, vitae posuere erat. Nulla sollicitudin blandit nunc, vel scelerisque orci vehicula eu. Nam sit amet sapien lectus.';
const image = 'http://via.placeholder.com/300x120';
return <div className={classes.root}>
<GridList className={classes.gridList} justify="center" cellHeight={300} cols={3} spacing={32}>
<GridListTile>
<Company title={title}
description={description}
image={image}/>
</GridListTile>
</GridList>
</div>
};
export default withStyles(styles)(CompanyList);
Note: Ensure that you save the file.
import CompanyList from './CompanyList';
export default CompanyList;
import CompanyList from './CompanyList';
{/*CompanyList placeholder*/}
<CompanyList/>
Next, use GraphiQL to get familiar with Jahia GraphQL queries before integrating them in your app. This example shows how to execute queries on the nodesByQuery query field.
To build queries with GraphiQL:
{
jcr(workspace:LIVE) {
nodesByQuery(query: "SELECT * FROM [jdnt:company] as results WHERE ISDESCENDANTNODE(results, '/sites/digitall/')", queryLanguage:SQL2) {
nodes {
uuid
name
}
}
}
}
Note that you are using an SQL2 query to retrieve the data.
{
jcr(workspace:LIVE) {
nodesByQuery(query: "SELECT * FROM [jdnt:company] as results WHERE ISDESCENDANTNODE(results, '/sites/digitall/')", queryLanguage:SQL2) {
nodes {
uuid
name
properties {
name
value
}
}
}
}
}
{
jcr(workspace:LIVE) {
nodesByQuery(query: "SELECT * FROM [jdnt:company] as results WHERE ISDESCENDANTNODE(results, '/sites/digitall/')", queryLanguage:SQL2) {
nodes {
uuid
name
properties(language: "en") {
name
value
}
}
}
}
}
{
jcr(workspace:LIVE) {
nodesByQuery(query: "SELECT * FROM [jdnt:company] as results WHERE ISDESCENDANTNODE(results, '/sites/digitall/')", queryLanguage:SQL2) {
nodes {
uuid
name
title: displayName(language : "en")
description : property(name : "overview", language : "en") {
value
}
thumbnail : property(name : "thumbnail", language : "en") {
url: refNode {
path
}
}
}
}
}
}
Next, add the Apollo GraphQL client library to be able to connect to Jahia’s GraphQL API.
To add the Apollo GraphQL client library:
yarn add react-apollo
yarn add apollo-cache-inmemory
yarn add apollo-client
yarn add apollo-client-preset
yarn add apollo-link-rest
yarn add graphql
//...
import {ApolloProvider} from 'react-apollo';
import {ApolloClient} from 'apollo-client';
import {HttpLink} from 'apollo-link-http';
import {InMemoryCache} from 'apollo-cache-inmemory';
const JWTDXToken = 'JWT_DX_TOKEN';
const httpLink = new HttpLink({
uri: 'http://localhost:8080/modules/graphql',
headers: {
'Authorization': `Bearer ${JWTDXToken}`
}
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
});
(<React.Fragment>
<ApolloProvider client={client}>
<CssBaseline/>
<AppBar position="static" color="default">
<Toolbar>
<Typography variant="h6" component="h1" color="inherit">
Companies
</Typography>
</Toolbar>
</AppBar>
<Grid container>
<Grid item xs={12}>
<Paper>
<CompanyList/>
</Paper>
</Grid>
</Grid>
</ApolloProvider>
</React.Fragment>);
import React from 'react';
import {Query} from "react-apollo";
import gql from 'graphql-tag';
import CompanyList from './CompanyList';
const COMPANIES_QUERY = gql`
query CompaniesListQuery($language: String) {
jcr(workspace:LIVE) {
nodesByQuery(query: "SELECT * FROM [jdnt:company] as results WHERE ISDESCENDANTNODE(results, '/sites/digitall/')", queryLanguage:SQL2) {
nodes {
uuid
title: displayName(language : $language)
description : property(name : "overview", language : $language) {
value
}
thumbnail : property(name : "thumbnail", language : $language) {
url: refNode {
path
}
}
}
}
}
}
`;
const CompanyListContainer = () => {
const variables = {
language: "en"
};
const generateURL = path => {
return `http://localhost:8080/files/live${path}?t=thumbnail2`;
};
return (
<Query query={COMPANIES_QUERY} variables={variables} fetchPolicy="network-only">
{({loading, data}) => {
let companies = [];
if (data && data.jcr && data.jcr.nodesByQuery) {
//Build the company data as expected by the Company component
data.jcr.nodesByQuery.nodes.forEach(node => {
companies.push({
id: node.uuid,
title: node.title,
description: node.description.value,
image: generateURL(node.thumbnail.url.path)
})
});
}
return <CompanyList loading={loading}
companies={companies}/>
}}
</Query>
);
};
export default CompanyListContainer;
import CompanyList from './CompanyList';
To:
import CompanyList from './CompanyList.container';
<CardContent>
<Typography component="h1" variant="h4">
Company Name
</Typography>
<br/>
<Typography component="p">
{description.length > 150 ? `${description.substr(0,100)}...` : description}
</Typography>
</CardContent>
To:
...
<CardContent>
<Typography variant="h6">
{title}
</Typography>
<br/>
<Typography component="div">
<p dangerouslySetInnerHTML={{__html: description.length > 150 ? `${description.substr(0, 100)}...` : description}}/>
</Typography>
</CardContent>
...
You now have a base Javascript application that retrieves content from Jahia using GraphQL and that uses React and React Material to display the content.By default, Jahia’s REST and GraphQL API are closed even if nodes have public read permissions. The APIs are closed for security reasons. To open up the API and make it usable for your application, you must configure Jahia’s security filter module to allow public access. You can find more information about the security filter module here: https://github.com/Jahia/security-filter
To make the data publicly accessible:
org.jahia.modules.api.permissions-myapp.cfg
Jahia configuration file to the digital-factory-data/karaf/etc folder with the following contents:
permission.myapp.api=graphql
permission.myapp.scope=myapp
permission.myapp.nodeType=jnt:news,jnt:contentFolder,rep:root
permission.myapp.pathPattern=/,/sites/[^/]+/contents/.*,/sites/[^/]+/files/.*
The scope setup here requires creating a JWT token when we integrate authorization in the Apollo Client in the Javascript code. Please be aware that the value of permission.myapp.pathPattern here, it should match the node paths which you are going to access.org.jahia.modules.graphql.provider-myapp.cfg
file for the CORS authorization with the following content:
http.cors.allow-origin=http://localhost:3000
Scopes : myapp
Referrer : (empty)
IPs : (empty)
Next, create and publish sample content from Content and Media Manager so that you have content to browse and display in your Javascript application.
To create and publish sample content:
my-app
.Learn more about Content and Media Manager in the Academy.
In this tutorial, you: