Create and Fetch Content From Sitecore Content Hub One using GraphQL and React
Hello everyone, In this blog, we will see overview of Sitecore Content Hub One product its benefits, features etc.
This blog will walkthrough below listed points.
- Overview
- Features
- Content Hub One Dashboard
- Creating Taxonomy
- Creating Content Type
- Uploading New Media
- Creating Content Items From Content Type
- Getting GraphQL Token
- Creating Sample React Application To Read the Content from Content Hub One
Overview
Sitecore Content Hub One is a powerful platform. It is designed to centralize and streamline content creation, management, and distribution across various channels.Focuses on delivering content via APIs, enabling seamless integration with various front-end frameworks.Hosted in the cloud, offering scalability, automatic updates, and reduced infrastructure management.Provides flexible APIs and SDKs for developers to integrate and build custom solutions efficiently.
Features
- Headless Content Management : Allows content to be delivered via APIs to any front-end or platform, supporting omnichannel experiences.
- Content Modeling : Flexible content model design, enabling customized content structures that suit specific business needs.
- User-Friendly Interface : Offers a simple and intuitive user interface for content creators and marketers, making content creation and management easier.
- API-Driven : Provides REST and GraphQL APIs, enabling developers to fetch and manage content programmatically
Content Hub One Dashboard
There are four main sections. Model Content, Create Content, Upload Media, Connect To Channels
Creating Taxonomy
Go to Create Content Type --> You will get Taxonomy option in dropdown.This option will help you to create taxonomy for better categorization of your content. Follow below screesnhots for creating the same.This is just key value pair.
Creating Content Type
This option help you to create different content types. This is exactly similar to creating template in Sitecore.Follow below screenshot for your reference. We have created Blogposts content type. The below screenshot also helps in understanding how we can add new fields.
Uploading Media
You can use uplaod media option to upload your media file into content hub one. Just point to remember is to published the media to edge.Once media is uploaded you can see all media in media section.
Creating Content Items From Content Type
This option helps us to create actual data items from the content types.Once created,remember to publish it. In our case, its Blogposts.Follow below screenshots for your references.
Getting GraphQL Token
GraphQL token is important ti authenticate a request or user to fetch the data from Content Hub One. To get the token, Navigate to Settings --> API Keys --> Create API Keys. Follow below screeshot for your references.
Now once the key is generated, we can access the GraphQL Preview IDE to check everything is working fine.For GraphQL query, refer the references section at the end of the blog.
Creating Sample React Application To Read the Content from Content Hub One
Now its time to fetch all the data items created from BlogPosts Content type in front end application. Here we will create a sample react application. We will also try to implement the facet filter based on taxonomy.
Point to note, we have starter kit built in NextJS for Sitecore Content Hub One. Link
Let install dependencies which we are going to use in this current project(@apollo/client, react-router-dom). I am not going in detail of setting up react application. You can follow react docs for your references here
Let's setup our folder structure as shown in below screenshot.
Set up ApolloClient.js to interact with Content Hub One using GraphQL Token. In my case, I am getting the GraphQL token from .env.local file
//src/ApolloClient.js
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: process.env.REACT_APP_GraphlqPreviewEndpoint, // Replace with your GraphQL endpoint
headers: {
'X-GQL-Token': process.env.REACT_APP_Contenthubone_Graphqltoken
// Replace with your API key
},
}),
cache: new InMemoryCache(),
});
export default client;
Now let's write down all the GraphQL queries required for our implementation in GraphQLQueries.js
import { gql } from '@apollo/client';
const MEDIA_QUERY = `
id
name
fileName
fileUrl
description
fileWidth
fileHeight
fileId
fileSize
fileType
`;
export const GET_SINGLE_BLOGPOST=gql`
query($blogpostid:String!) {
demoPage(id:$blogpostid){
id
name
title
author
description
locale
blogtype {
results { id }
}
blogimage {
total
results{
${MEDIA_QUERY}
}
}
relatedblog {
results { ... on DemoPage { id title name } }
}
}
}
`
export const GET_ALL_BLOG_POSTS = gql`
query getallblogposts($blogTypeId: String) {
allDemoPage (where:{
AND:[
{ blogtype:{taxonomy_blogposttype_ids: $blogTypeId}}
]
}){
results {
id
name
title
description
author
locale
blogtype {
results { id }
}
blogimage {
total
results{
${MEDIA_QUERY}
}
}
relatedblog {
results { ... on DemoPage { id title name } }
}
}
}
}
`;
export const GET_ALL_TAXONOMY = gql`
query {
allTaxonomy_blogposttype {
results { id }
}
}
`;
Let's design HomePage.js which will list down all the data items recieved from Content Hub One. This will have the logic of querying the Content Hub One using apollo client. This component will also have the facet component for filtering purpose.
import React, { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_ALL_BLOG_POSTS, GET_ALL_TAXONOMY } from '../helper/GraphqlQueries';
import BlogPostCard from '../components/BlogPostCard';
import FacetFilter from '../components/FacetTaxonomyFilter';
const HomePage = () => {
const { data: taxonomyData } = useQuery(GET_ALL_TAXONOMY);
const [filter, setFilter] = useState(null);
const { loading, error, data, refetch } = useQuery(GET_ALL_BLOG_POSTS, {
variables: filter ? { blogTypeId: filter } : {},
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
// const filteredPosts = data.allDemoPage.results.filter(post =>
// !filter || post.blogtype.results.some(tag => tag.id === filter)
/// );
const allBlogPosts = data.allDemoPage.results;
console.log(allBlogPosts);
console.log(taxonomyData);
const handleSelect = (id) => {
setFilter(id);
refetch({ blogTypeId: id });
};
return (
<div>
<FacetFilter taxonomies={taxonomyData.allTaxonomy_blogposttype.results} onFilterSelect={handleSelect} />
<br/>
<div className="container">
{allBlogPosts.map(post => (
<BlogPostCard key={post.id} post={post} />
))}
</div>
</div>
);
};
export default HomePage;
Let's code BlogPostCard.js Component which will have the card markup of our blog item
import React from 'react';
import { Link } from 'react-router-dom';
import { renderDocContent } from '../helper/RenderRichTextContent';
const BlogPostCard = ({ post }) => {
const blogtag = post.blogtype.results.map(tag => (
<span key={tag.id} className="tag">{tag.id.split('_').pop()}</span>
))
return (
<div className="card">
<div className="innercard">
<img style={{objectFit:'cover', height:'200px', width:'100%',objectPosition:'center'}}
src={post.blogimage?.results[0]?.fileUrl} alt={post.blogimage?.results[0]?.description} />
<div className='innercardcontent'>
<div className="blogtypecategory">{blogtag}</div>
<h1 className='headertitle'>{post.title}</h1>
{renderDocContent(post.description)}
<div className="spanparentclass">
<span>{post.author}</span><span><Link to={ `/detail/${post.id}`}>
View Details</Link>
</span>
</div>
</div>
</div>
</div>
);
};
export default BlogPostCard;
Let's design Filter Taxonomy component in FacetTaxonomyFilter.js
import React, { useState } from 'react';
import '../FacetFilter.css'
const FacetFilter = ({ taxonomies, onFilterSelect }) => {
const [selected, setSelected] = useState(null);
const handleSelect = (id) => {
setSelected(id);
onFilterSelect(id);
};
return (
<div className="facet-filter-container">
<h3 className="facet-header">Filter by Blog Type</h3>
<div className="facet-options">
{taxonomies.map((tax) => (
<span
key={tax.id}
className={`facet-item ${selected === tax.id ? 'selected' : ''}`}
onClick={() => handleSelect(tax.id)}
>
{tax.id.split('_').pop()}
</span>
))}
<button onClick={() => handleSelect(null)} className="clear-filter-button">Clear Filter</button>
</div>
</div>
);
};
export default FacetFilter;
Let's design the Blog Detail page in BlogDetailPage.js
import React from 'react';
import { useParams } from 'react-router-dom';
import { useQuery } from '@apollo/client';
import {GET_SINGLE_BLOGPOST} from '../helper/GraphqlQueries'
import { renderDocContent } from '../helper/RenderRichTextContent';
import '../App.css';
import {Link} from 'react-router-dom'
const BlogDetailPage = () => {
const { id } = useParams();
const { data: blogpostdata, loading, error } = useQuery(GET_SINGLE_BLOGPOST,{
variables : { blogpostid: id }
});
console.log("the data inside detail page",blogpostdata)
if (loading) return <p>Loading...</p>;
if (error || !blogpostdata) return <p>Error in Loading...</p>;
const blogtag = blogpostdata.demoPage?.blogtype?.results?.map(tag => (
<span key={tag.id} className="tag-detail">{tag.id.split('_').pop()}</span>
));
return (
<div className='detailcontainer'>
<img style={{objectFit:'cover', height:'400px', width:'100%',objectPosition:'center',marginBottom:'5px'}} src={blogpostdata.demoPage.blogimage?.results[0]?.fileUrl} alt={blogpostdata.demoPage.blogimage?.results[0]?.description} />
<div class="innerdetailcontainer">
{blogtag}
<h1>{blogpostdata.demoPage.title}</h1>
{renderDocContent(blogpostdata.demoPage.description)}
<div style={{float:'right'}}>
{`Author : ${blogpostdata.demoPage.author}`}
</div>
</div>
<div class="relatedcontent">
<h3>Related Blogs</h3>
<ul className='ulrelated'>
{blogpostdata.demoPage.relatedblog.results?.map(related => (
<li key={related.id}><Link to={`/detail/${related.id}`}>{related.title}</Link></li>
))}
</ul>
<Link to='/' className="returnhomeclass">{"Back To Home"}</Link>
</div>
</div>
)
};
export default BlogDetailPage;
Now we need to write a function for rendering rich text content coming from Sitecore Content Hub One. This will be done in RenderRichTextContent.js
export const renderDocContent = (doc) => {
return doc.content.map((block, index) => {
if (block.type === 'paragraph') {
return (
<p key={index}>
{block.content.map((textBlock, textIndex) => {
if (textBlock.type === 'text') {
return (
<span key={textIndex} style={{ fontWeight: textBlock.marks?.[0]?.type === 'bold' ? 'bold' : 'normal' }}>
{textBlock.text}
</span>
);
}
return null;
})}
</p>
);
}
return null;
});
};
Now we will define the Routes in root App component in App.js
import './App.css';
import { ApolloProvider } from '@apollo/client';
import client from './ApolloClient';
import HomePage from './pages/HomePage';
import BlogDetailPage from './pages/BlogDetailPage'
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
function App() {
return (
<ApolloProvider client={client}>
<div className="App">
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/detail/:id" element={<BlogDetailPage />} />
</Routes>
</Router>
</div>
</ApolloProvider>
);
}
export default App;
Now lets add our css to App.css and FacetFilter.css
//App.css
*{
box-sizing:border-box;
padding:0px;
margin:0px;
font-family: "Times New Roman", Times, serif;
}
.container{
display: flex;
flex-wrap: wrap;
}
.card{
width: 33.33%;
padding: 1rem;}
.innercard{
overflow: hidden;
}
.innercardcontent{
padding: 10px;
}
.tag{
font-size: 1rem;
padding-bottom: 5px;
color:brown;
text-transform: capitalize;
}
.returnhomeclass{
float:right;
}
.innercardcontent span{
padding-right: 15px;
}
.innercardcontent .headertitle{
padding-bottom: 10px;
}
.innercardcontent p{
font-size: 1rem !important;
text-align:justify;
}
.spanparentclass{
display: flex;
justify-content: space-between;
padding-top: 10px;
}
.spanparentclass span{
font-size: 16px;
color: black;
}
.spanparentclass .readmorelinkclass{
cursor:pointer;
text-decoration: underline;
}
.innerdetailcontainer p{
padding:5px;
margin-top:5px;
margin-bottom:3px;
}
.relatedcontent{
padding:5px;
}
.detailcontainer{
width:80%;
margin:auto;
}
.ulrelated{
padding: 5px;
}
.tag-detail{
font-size: 1.5rem;
padding-bottom: 5px;
color:brown;
text-transform: capitalize;
}
@media only screen and (max-width:600px) {
.container{
flex-direction: column;
}
.card{
width:100%;
}
}
//FacetFilter.css
.facet-filter-container {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
margin-top:20px;
}
.facet-header {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 10px;
}
.facet-options {
display: flex;
align-items: center;
gap: 15px;
}
.facet-item {
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
background-color: #f5f5f5;
}
.facet-item.selected {
background-color: #007bff;
color: white;
}
.clear-filter-button {
padding: 8px 12px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.clear-filter-button:hover {
background-color: #c0392b;
}
Finally, we can see out output by starting the react application using npm start
Home Page with all results.
Home Page with Filtered Output Reports:
Detail Page:
Hope you have liked this blog. Keep Learning!!
You can check my other blogs too if interested. Blog Website
References:
Comments
Post a Comment