Create a React site using data from Content Graph
Content Graph & React with strongly typed content
This tutorial creates a website that uses contents from the "Music Festival" site, that is synchronized over to an Optimizely Content Graph account. Use a tool to auto generate Typescript types from our Content Graph service to let you work strongly typed against the content from the "Music Festival" site.
This tutorial uses a pre-pepared Content Graph account, where content types and content are synchronized from a "Music Festival" site. You can do the same thing from any Optimizely Content Management System (CMS) site using your own account. You will only have to modify the key and the queries.
Preparation
- Setting up the Music Festival CMS backend site (https://github.com/episerver/content-delivery-js-sdk/tree/master/samples/music-festival-vue-decoupled/backend).
- Added the package "Optimizely.ContentGraph.Cms" to the site, see https://docs.developers.optimizely.com/digital-experience-platform/v1.4.0-content-graph/docs/installation-and-configuration
- Configured keys to "Content Graph" for the site, see https://docs.developers.optimizely.com/digital-experience-platform/v1.4.0-content-graph/docs/installation-and-configuration
- Run the scheduled job "Content Graph" to synchronize all content types and content from the site to "Content Graph", see https://docs.developers.optimizely.com/digital-experience-platform/v1.4.0-content-graph/docs/scheduled-synchronization
Tutorial
-
Create the site with the
yarn create react-app music-festival-graphql --template typescript
command. -
Go to the folder for the created site with the
cd music-festival-graphql
command. -
Start the site with the
yarn start
command. -
Go to
http://localhost:3000
. -
Install
@tanstack/react-query
with theyarn add @tanstack/react-query
command. -
Install
html-react-parser
with theyarn add html-react-parser
command. -
Add Codegen dependencies. Codegen lets you auto-generate TypeScript types based on the schema for the site from which you synchronized content types and content.
yarn add graphql yarn add @graphql-codegen/cli yarn add @graphql-codegen/typescript yarn add @graphql-codegen/typescript-operations yarn add @graphql-codegen/typescript-react-query
-
Add codegen.yaml to root with the
touch codegen.yaml
command. -
Add the following configuration to codegen.yaml.
schema: https://cg.optimizely.com/content/v2?auth=XoXsOl2bScWZzguDUylvaaZ5PEAAH5tg5qme0NebCHzEpyO4 documents: './src/**/*.graphql' generates: ./src/generated.ts: plugins: - typescript - typescript-operations - typescript-react-query config: withHooks: true fetcher: endpoint: 'https://cg.optimizely.com/content/v2?auth=XoXsOl2bScWZzguDUylvaaZ5PEAAH5tg5qme0NebCHzEpyO4'
XoXsOl2bScWZzguDUylvaaZ5PEAAH5tg5qme0NebCHzEpyO4
is the public key for the Content Graph account that you will use. You can change the yaml file to use your own public key, if you have prepared a CMS backend site (see preparations section). -
Update package.json by adding the following inside "scripts":
"generate": "graphql-codegen"
The package.json might look like similar to the following after the modification:{ "name": "music-festival-graphql", "version": "0.1.0", "private": true, "dependencies": { "@graphql-codegen/cli": "^2.11.6", "@graphql-codegen/typescript": "^2.7.3", "@graphql-codegen/typescript-operations": "^2.5.3", "@graphql-codegen/typescript-react-query": "^4.0.1", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "graphql": "^16.6.0", "html-react-parser": "^3.0.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-query": "^3.39.2", "react-scripts": "5.0.1", "typescript": "^4.4.2", "web-vitals": "^2.1.0" }, "scripts": { "generate": "graphql-codegen", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
-
Optional: Install extension in Visual Studio Code. Installing graphql.vscode-graphql as an extension to Visual Studio (VS) Code will make it easier to write GraphQL queries inside VS Code. Go to extensions and search for:
graphql.vscode-graphql
. -
Create folders for graphql under src with the
mkdir src/graphql/fragments
command. -
Add one GraphQL query to used to handle requests for the pages. The idea is to use the same query for incoming routes, but only fetch the necessary properties that matches the route. You start to create fragments (partial queries) to use from your main query. Creating fragments lets you reuse GraphQL code for several queries. The query (including fragments) is validated against the schema when you run the tool for auto-creation of TypeScript types. Error messages are returned if your query has issues. You can also test to create your own queries in our online IDE using the account.
-
Go to fragments folder with the
cd src/graphql/fragments
command. -
Create LandingPageBlockData.graphql with the
touch LandingPageBlockData.graphql
command.fragment LandingPageBlockData on LandingPageBlockData { Heading Message }
-
Create ContentBlock.graphql with the
touch ContentBlock.graphql
command.fragment ContentBlock on ContentBlock { Title Image ImageAlignment Content }
-
Create ImageFile.graphql with the
touch ImageFile.graphql
command.fragment ImageFile on ImageFile { Thumbnail { Url } Content Url }
-
Create ItemsInContentArea.graphql with the
touch ItemsInContentArea.graphql
command.fragment ItemsInContentArea on IContent { __typename ...on ContentBlock { ...ContentBlock } ...on ImageFile { ...ImageFile } }
-
Create ArtistDetailsPage.graphql with the
touch ArtistDetailsPage.graphql
command.fragment ArtistDetailsPage on ArtistDetailsPage { PerformanceStartTime PerformanceEndTime StageName ArtistName ArtistPhoto ArtistGenre ArtistDescription ArtistIsHeadliner Name RelativePath }
-
Create LandingPage.graphql with the
touch LandingPage.graphql
command.fragment LandingPage on LandingPage { Title Subtitle BuyTicketBlock { ...LandingPageBlockData } HeroImage MainContentArea { ContentLink { Expanded { ...ItemsInContentArea } } } FooterContentArea { ContentLink { Expanded { ...ItemsInContentArea } } } }
-
Go to graphql folder with the
cd..
command. -
Create Start.graphql with the
touch start,graphql
command.query Start( $relativePath: String $locale: [Locales] = en $stageName: String $artistGenre: String ) { Content( locale: $locale where: { RelativePath: { eq: $relativePath } } limit: 1 ) { items { Name Url __typename RelativePath ... on LandingPage { ...LandingPage _children { ArtistContainerPage { items { Name RelativePath headlines: _children { ArtistDetailsPage( where: { ArtistIsHeadliner: { eq: true } } orderBy: { PerformanceStartTime: ASC, Name: ASC } ) { items { ...ArtistDetailsPage } } } } } } } ... on ArtistContainerPage { Name RelativePath artists: _children { ArtistDetailsPage( where: { StageName: { eq: $stageName } ArtistGenre: { eq: $artistGenre } } orderBy: { ArtistIsHeadliner: ASC, PerformanceStartTime: ASC, StageName: ASC, Name: ASC } ) { items { ...ArtistDetailsPage } facets { ArtistGenre(orderType: VALUE, orderBy: ASC, limit: 10) { name count } StageName(orderType: VALUE, orderBy: ASC, limit: 10) { name count } } } } } ... on ArtistDetailsPage { ...ArtistDetailsPage } } } }
-
Run the
yarn generate
command to generate TypeScript classes by tool from Content Graph using the created queries (update gerated.ts). -
Check the generated types in generated.ts. You can use those classes when creating the website, to have strongly typed support, similar to working with content types in .Net.
Implement the site
Create a starting point for the site, where you make request to Content Graph with the Start.graphql
query.
- Update index.tsx to use react-query.
import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient() const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement ); root.render( <QueryClientProvider client={queryClient}> <App/> </QueryClientProvider> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
- Update App.tsx to use react-query.
import './App.css'; import { Locales, useStartQuery } from './generated'; const App = () => { let relativePath = window.location.pathname.length > 1 ? window.location.pathname : '/en' if(relativePath.endsWith('/')) { relativePath = relativePath.substring(0, relativePath.length - 1) } const urlSegments = relativePath.split('/') const language = urlSegments.length == 0 ? 'en' : (urlSegments[0].length > 0 ? urlSegments[0] : (urlSegments.length > 1 ? urlSegments[1] : 'en')) const locale = language.replace('-', '_') const { data } = useStartQuery({ relativePath: relativePath, locale: locale as Locales }); if(data) { return ( <div className="App"> { data?.Content?.items?.map((content, idx) => { const siteUrl = content?.Url?.substring(0, content?.Url.length - 1 - (content?.RelativePath?.length ?? 0)) ?? '' if(content?.__typename === 'LandingPage') { return ( <div className="row" key={idx}> </div> ) } else if (content?.__typename === 'ArtistContainerPage') { return ( <div className="row" key={idx}> </div> ) } else if (content?.__typename === 'ArtistDetailsPage') { return ( <div className="row" key={idx}> </div> ) } }) } </div> ); } return ( <div className="App"> Loading </div> ); }; export default App;
Use the typed content to continue creating the site
You created the base. You can now use the data received from the service in a strongly typed way.
-
Build the site with the
yarn build
command. -
Start the site with the
yarn start
command.
You can now browse to the different parts of the site. App.tsx is called for them, and content contains data based on which URL that is being used. You can try the following URLs:http://localhost:3000
http://localhost:3000/en
http://localhost:3000/en/artists
http://localhost:3000/en/artists/windy-of-trick
The page will not render any content yet, because we have not used any content yet.
-
Add bootstrap with the
yarn add bootstrap
command. -
Update App.tsx to implement LandingPage content.
import './App.css'; import { Locales, useStartQuery } from './generated'; import parse from 'html-react-parser' import 'bootstrap/dist/css/bootstrap.min.css' const App = () => { let relativePath = window.location.pathname.length > 1 ? window.location.pathname : '/en' if(relativePath.endsWith('/')) { relativePath = relativePath.substring(0, relativePath.length - 1) } const urlSegments = relativePath.split('/') const language = urlSegments.length == 0 ? 'en' : (urlSegments[0].length > 0 ? urlSegments[0] : (urlSegments.length > 1 ? urlSegments[1] : 'en')) const locale = language.replace('-', '_') const { data } = useStartQuery({ relativePath: relativePath, locale: locale as Locales }); if(data) { return ( <div className="App"> { data?.Content?.items?.map((content, idx) => { const siteUrl = content?.Url?.substring(0, content?.Url.length - 1 - (content?.RelativePath?.length ?? 0)) ?? '' if(content?.__typename === 'LandingPage') { return ( <div className="row" key={idx}> <figure className="figure"> <img src={siteUrl + content?.HeroImage ?? ''} className="figure-img img-fluid rounded" alt={content?.Title ?? ''}/> <figcaption className="figure-caption">{content?.Title}</figcaption> <figcaption className="text-end">{content?.Subtitle}</figcaption> </figure> <div className="card text-dark bg-light mb-3"> <div className="card-header"> {content?.BuyTicketBlock?.Heading} </div> <div className="card-body"> {content?.BuyTicketBlock?.Message} </div> </div> { content?.MainContentArea?.map((mainContentAreaItem, mainContentAreaItemIdx) => { return ( <div className="col-lg-3 col-md-4 col-sm-6 mb-3" key={mainContentAreaItemIdx}> {(() => { const contentItem = mainContentAreaItem?.ContentLink?.Expanded if (contentItem?.__typename === "ContentBlock"){ return ( <div className="card h-100"> <h4 className="mb-3">{contentItem?.Title}</h4> <div className="card-body"> <img src={ siteUrl + contentItem?.Image } className="img-fluid rounded-start" alt={ contentItem?.Title ?? '' }/> </div> <div className="card-body"> { parse(contentItem?.Content ?? '') } </div> </div> ) } if (contentItem?.__typename === "ImageFile"){ return ( <div className="card h-100"> <div className="card-body"> <img src={ siteUrl + contentItem?.Url} className="img-fluid rounded-start" alt={contentItem?.Url ?? ''}/> </div> </div> ) } return ( <div className="card h-100"></div> ) })()} </div> ) })} { content._children?.ArtistContainerPage?.items?.map((artistContainerPage, artistContainerPageIdx) => { return ( <div className="row" key={artistContainerPageIdx}> <div className="text-dark bg-light mb-3"> <div className="card-header"> <h4><a href={ artistContainerPage?.RelativePath ?? "" } >{ artistContainerPage?.Name }</a></h4> </div> </div> <div className="card text-dark bg-light mb-3"> <div className="card-header">Headlines</div> </div> { artistContainerPage?.headlines?.ArtistDetailsPage?.items?.map((artistDetailsPage, artistDetailsPageIdx) => { return ( <div className="col-lg-3 col-md-4 col-sm-6 mb-3" key={artistDetailsPageIdx}> <div className="card h-100"> <h4 className="mb-3"><a href={artistDetailsPage?.RelativePath ?? ''}>{artistDetailsPage?.ArtistName}</a></h4> <div className="card-body"> { parse(artistDetailsPage?.ArtistDescription ?? '')} </div> </div> </div> ) })} </div> ) })} </div> ) } else if (content?.__typename === 'ArtistContainerPage') { return ( <div className="row" key={idx}> </div> ) } else if (content?.__typename === 'ArtistDetailsPage') { return ( <div className="row" key={idx}> </div> ) } }) } </div> ); } return ( <div className="App"> Loading </div> ); }; export default App;
You can now browse to root of the site to check the content.
http://localhost:3000
http://localhost:3000/en
-
Update App.tsx to implement Artist container page content.
import './App.css'; import { Locales, useStartQuery } from './generated'; import parse from 'html-react-parser' import 'bootstrap/dist/css/bootstrap.min.css' const App = () => { let relativePath = window.location.pathname.length > 1 ? window.location.pathname : '/en' if(relativePath.endsWith('/')) { relativePath = relativePath.substring(0, relativePath.length - 1) } const urlSegments = relativePath.split('/') const language = urlSegments.length == 0 ? 'en' : (urlSegments[0].length > 0 ? urlSegments[0] : (urlSegments.length > 1 ? urlSegments[1] : 'en')) const locale = language.replace('-', '_') const { data } = useStartQuery({ relativePath: relativePath, locale: locale as Locales }); if(data) { return ( <div className="App"> { data?.Content?.items?.map((content, idx) => { const siteUrl = content?.Url?.substring(0, content?.Url.length - 1 - (content?.RelativePath?.length ?? 0)) ?? '' if(content?.__typename === 'LandingPage') { return ( <div className="row" key={idx}> <figure className="figure"> <img src={siteUrl + content?.HeroImage ?? ''} className="figure-img img-fluid rounded" alt={content?.Title ?? ''}/> <figcaption className="figure-caption">{content?.Title}</figcaption> <figcaption className="text-end">{content?.Subtitle}</figcaption> </figure> <div className="card text-dark bg-light mb-3"> <div className="card-header"> {content?.BuyTicketBlock?.Heading} </div> <div className="card-body"> {content?.BuyTicketBlock?.Message} </div> </div> { content?.MainContentArea?.map((mainContentAreaItem, mainContentAreaItemIdx) => { return ( <div className="col-lg-3 col-md-4 col-sm-6 mb-3" key={mainContentAreaItemIdx}> {(() => { const contentItem = mainContentAreaItem?.ContentLink?.Expanded if (contentItem?.__typename === "ContentBlock"){ return ( <div className="card h-100"> <h4 className="mb-3">{contentItem?.Title}</h4> <div className="card-body"> <img src={ siteUrl + contentItem?.Image } className="img-fluid rounded-start" alt={ contentItem?.Title ?? '' }/> </div> <div className="card-body"> { parse(contentItem?.Content ?? '') } </div> </div> ) } if (contentItem?.__typename === "ImageFile"){ return ( <div className="card h-100"> <div className="card-body"> <img src={ siteUrl + contentItem?.Url} className="img-fluid rounded-start" alt={contentItem?.Url ?? ''}/> </div> </div> ) } return ( <div className="card h-100"></div> ) })()} </div> ) })} { content._children?.ArtistContainerPage?.items?.map((artistContainerPage, artistContainerPageIdx) => { return ( <div className="row" key={artistContainerPageIdx}> <div className="text-dark bg-light mb-3"> <div className="card-header"> <h4><a href={ artistContainerPage?.RelativePath ?? "" } >{ artistContainerPage?.Name }</a></h4> </div> </div> <div className="card text-dark bg-light mb-3"> <div className="card-header">Headlines</div> </div> { artistContainerPage?.headlines?.ArtistDetailsPage?.items?.map((artistDetailsPage, artistDetailsPageIdx) => { return ( <div className="col-lg-3 col-md-4 col-sm-6 mb-3" key={artistDetailsPageIdx}> <div className="card h-100"> <h4 className="mb-3"><a href={artistDetailsPage?.RelativePath ?? ''}>{artistDetailsPage?.ArtistName}</a></h4> <div className="card-body"> { parse(artistDetailsPage?.ArtistDescription ?? '')} </div> </div> </div> ) })} </div> ) })} </div> ) } else if (content?.__typename === 'ArtistContainerPage') { return ( <div className="row" key={idx}> <h3 className="mb-3">{content?.Name}</h3> <div className="col-lg-3 col-md-4 col-sm-6 mb-3"> <div className="list-group list-group-flush"> <strong className="facet-title">Artist Genre</strong> <select id='artistGenre' className="form-select"> <option>All</option> { content.artists?.ArtistDetailsPage?.facets?.ArtistGenre?.map((artistGenreFacet) => { return ( <option value={artistGenreFacet?.name?.toString()}> {artistGenreFacet?.name} ({artistGenreFacet?.count}) </option> ) })} </select> </div> <div className="list-group list-group-flush"> <strong className="facet-title">Stage Name</strong> <select id='stageName' className="form-select"> <option value=''>All</option> { content.artists?.ArtistDetailsPage?.facets?.StageName?.map((stageNameFacet) => { return ( <option value={stageNameFacet?.name?.toString()}> {stageNameFacet?.name} ({stageNameFacet?.count}) </option> ) })} </select> </div> </div> { content.artists?.ArtistDetailsPage?.items?.map((artistDetailsPage, artistDetailsPageIdx) => { return ( <div className="card mb-3"> <div className="row g-0" key={artistDetailsPageIdx}> <div className="col-md-4"> {(() => { if (artistDetailsPage?.ArtistPhoto && artistDetailsPage.ArtistPhoto.length > 0){ return ( <img src={ siteUrl + artistDetailsPage?.ArtistPhoto } className="img-fluid rounded-start" alt={artistDetailsPage?.ArtistName ?? ''}/> ) } })()} </div> <div className="col-md-8"> <div className="card-body"> <div className="card-text">{ artistDetailsPage?.ArtistGenre } - { artistDetailsPage?.StageName }</div> <h5 className="card-title"><a href={artistDetailsPage?.RelativePath ?? ''}>{artistDetailsPage?.ArtistName}</a></h5> <div className="card-text">{ parse(artistDetailsPage?.ArtistDescription ?? '')}</div> <p className="card-text"><small className="text-muted">{artistDetailsPage?.PerformanceStartTime} - {artistDetailsPage?.PerformanceEndTime}</small></p> </div> </div> </div> </div> ) })} </div> ) } else if (content?.__typename === 'ArtistDetailsPage') { return ( <div className="row" key={idx}> </div> ) } }) } </div> ); } return ( <div className="App"> Loading </div> ); }; export default App;
Browse to artist page from the root of the site to check the content
http://localhost:3000/en/artists

Note
Filtering is not yet implemented. You can continue the implementation to add proper way of filtering artists based on genre and stage name. You can also add other filtering capabilities by modifying the query to add one more facet or a fulltext search box.
-
Update App.tsx to implement Artist details page content.
import './App.css'; import { Locales, useStartQuery } from './generated'; import parse from 'html-react-parser' import 'bootstrap/dist/css/bootstrap.min.css' const App = () => { let relativePath = window.location.pathname.length > 1 ? window.location.pathname : '/en' if(relativePath.endsWith('/')) { relativePath = relativePath.substring(0, relativePath.length - 1) } const urlSegments = relativePath.split('/') const language = urlSegments.length == 0 ? 'en' : (urlSegments[0].length > 0 ? urlSegments[0] : (urlSegments.length > 1 ? urlSegments[1] : 'en')) const locale = language.replace('-', '_') const { data } = useStartQuery({ relativePath: relativePath, locale: locale as Locales }); if(data) { return ( <div className="App"> { data?.Content?.items?.map((content, idx) => { const siteUrl = content?.Url?.substring(0, content?.Url.length - 1 - (content?.RelativePath?.length ?? 0)) ?? '' if(content?.__typename === 'LandingPage') { return ( <div className="row" key={idx}> <figure className="figure"> <img src={siteUrl + content?.HeroImage ?? ''} className="figure-img img-fluid rounded" alt={content?.Title ?? ''}/> <figcaption className="figure-caption">{content?.Title}</figcaption> <figcaption className="text-end">{content?.Subtitle}</figcaption> </figure> <div className="card text-dark bg-light mb-3"> <div className="card-header"> {content?.BuyTicketBlock?.Heading} </div> <div className="card-body"> {content?.BuyTicketBlock?.Message} </div> </div> { content?.MainContentArea?.map((mainContentAreaItem, mainContentAreaItemIdx) => { return ( <div className="col-lg-3 col-md-4 col-sm-6 mb-3" key={mainContentAreaItemIdx}> {(() => { const contentItem = mainContentAreaItem?.ContentLink?.Expanded if (contentItem?.__typename === "ContentBlock"){ return ( <div className="card h-100"> <h4 className="mb-3">{contentItem?.Title}</h4> <div className="card-body"> <img src={ siteUrl + contentItem?.Image } className="img-fluid rounded-start" alt={ contentItem?.Title ?? '' }/> </div> <div className="card-body"> { parse(contentItem?.Content ?? '') } </div> </div> ) } if (contentItem?.__typename === "ImageFile"){ return ( <div className="card h-100"> <div className="card-body"> <img src={ siteUrl + contentItem?.Url} className="img-fluid rounded-start" alt={contentItem?.Url ?? ''}/> </div> </div> ) } return ( <div className="card h-100"></div> ) })()} </div> ) })} { content._children?.ArtistContainerPage?.items?.map((artistContainerPage, artistContainerPageIdx) => { return ( <div className="row" key={artistContainerPageIdx}> <div className="text-dark bg-light mb-3"> <div className="card-header"> <h4><a href={ artistContainerPage?.RelativePath ?? "" } >{ artistContainerPage?.Name }</a></h4> </div> </div> <div className="card text-dark bg-light mb-3"> <div className="card-header">Headlines</div> </div> { artistContainerPage?.headlines?.ArtistDetailsPage?.items?.map((artistDetailsPage, artistDetailsPageIdx) => { return ( <div className="col-lg-3 col-md-4 col-sm-6 mb-3" key={artistDetailsPageIdx}> <div className="card h-100"> <h4 className="mb-3"><a href={artistDetailsPage?.RelativePath ?? ''}>{artistDetailsPage?.ArtistName}</a></h4> <div className="card-body"> { parse(artistDetailsPage?.ArtistDescription ?? '')} </div> </div> </div> ) })} </div> ) })} </div> ) } else if (content?.__typename === 'ArtistContainerPage') { return ( <div className="row" key={idx}> <h3 className="mb-3">{content?.Name}</h3> <div className="col-lg-3 col-md-4 col-sm-6 mb-3"> <div className="list-group list-group-flush"> <strong className="facet-title">Artist Genre</strong> <select id='artistGenre' className="form-select"> <option>All</option> { content.artists?.ArtistDetailsPage?.facets?.ArtistGenre?.map((artistGenreFacet) => { return ( <option value={artistGenreFacet?.name?.toString()}> {artistGenreFacet?.name} ({artistGenreFacet?.count}) </option> ) })} </select> </div> <div className="list-group list-group-flush"> <strong className="facet-title">Stage Name</strong> <select id='stageName' className="form-select"> <option value=''>All</option> { content.artists?.ArtistDetailsPage?.facets?.StageName?.map((stageNameFacet) => { return ( <option value={stageNameFacet?.name?.toString()}> {stageNameFacet?.name} ({stageNameFacet?.count}) </option> ) })} </select> </div> </div> { content.artists?.ArtistDetailsPage?.items?.map((artistDetailsPage, artistDetailsPageIdx) => { return ( <div className="card mb-3"> <div className="row g-0" key={artistDetailsPageIdx}> <div className="col-md-4"> {(() => { if (artistDetailsPage?.ArtistPhoto && artistDetailsPage.ArtistPhoto.length > 0){ return ( <img src={ siteUrl + artistDetailsPage?.ArtistPhoto } className="img-fluid rounded-start" alt={artistDetailsPage?.ArtistName ?? ''}/> ) } })()} </div> <div className="col-md-8"> <div className="card-body"> <div className="card-text">{ artistDetailsPage?.ArtistGenre } - { artistDetailsPage?.StageName }</div> <h5 className="card-title"><a href={artistDetailsPage?.RelativePath ?? ''}>{artistDetailsPage?.ArtistName}</a></h5> <div className="card-text">{ parse(artistDetailsPage?.ArtistDescription ?? '')}</div> <p className="card-text"><small className="text-muted">{artistDetailsPage?.PerformanceStartTime} - {artistDetailsPage?.PerformanceEndTime}</small></p> </div> </div> </div> </div> ) })} </div> ) } else if (content?.__typename === 'ArtistDetailsPage') { return ( <div className="row" key={idx}> <div className="card mb-3"> <div className="row g-0"> <div className="col-md-4"> {(() => { if (content?.ArtistPhoto && content.ArtistPhoto.length > 0){ return ( <img src={ siteUrl + content?.ArtistPhoto } className="img-fluid rounded-start" alt={content?.ArtistName ?? ''}/> ) } })()} </div> <div className="col-md-8"> <div className="card-body"> <div className="card-text">{ content?.ArtistGenre } - { content?.StageName }</div> <h5 className="card-title"><a href={content?.RelativePath ?? ''}>{content?.ArtistName}</a></h5> <div className="card-text">{ parse(content?.ArtistDescription ?? '')}</div> <p className="card-text"><small className="text-muted">{content?.PerformanceStartTime} - {content?.PerformanceEndTime}</small></p> </div> </div> </div> </div> </div> ) } }) } </div> ); } return ( <div className="App"> Loading </div> ); }; export default App;
You can now browse to the different artist pages, for example:
http://localhost:3000/en/artists/la-tangerine
http://localhost:3000/en/artists/top-reindeer
Updated 12 days ago