Create a React site using data from Optimizely Graph
Optimizely Graph & React with strongly typed content
This tutorial creates a website that uses content from the "Music Festival" site that is synchronized to an Optimizely Graph demo account. Use a tool to auto-generate Typescript types from the Optimizely Graph service to let you work with strongly typed content from the "Music Festival" site.
This tutorial uses a preprepared Optimizely 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.
Prerequisites
- Install yarn through npm:
npm install --global yarn
- Set up the Music Festival CMS backend site.
- Add the package "Optimizely.ContentGraph.Cms" to the site.
- Configure keys to "Optimizely Graph" for the site
- Run the scheduled job "Optimizely Graph" to sync all content types and content from the site to "Optimizely Graph".
Tutorial
Set up your CMS site
- Create your site by running:
yarn create react-app music-festival-graphql --template typescript
- Go to the new folder for your created site:
cd music-festival-graphql
- Start your site by running:
yarn start
- Go to your new site by going
http://localhost:3000
in your web browser. - Install
@tanstack/react-query
by running:
yarn add @tanstack/react-query@4
- Install
html-react-parser
by running:
yarn add html-react-parser
- Add
Codegen
dependencies. Codegen lets you auto-generate TypeScript types based on the schema for the site from which you sync 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
- Create a codegen.yaml file to root by running
touch codegen.yaml
- Add the following configuration to newly created codegen.yaml file.
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 Optimizely 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 the preparations section).
- Update the package.json file by adding the following inside "scripts":
"generate": "graphql-codegen"
Your package.json might look 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 writing GraphQL queries inside VS Code easier. Go to extensions and search for:
graphql.vscode-graphql
.
Add GraphQL files
-
Create folders for graphql and fragments under src by running:
cd src mkdir graphql cd graphql mkdir fragments
-
Add one GraphQL query to use to handle requests for the pages. The idea is to use the same query for incoming routes but only fetch the necessary properties that match the route. You start creating fragments (partial queries) 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 the auto-creation of TypeScript types. Error messages are returned if your query has issues. You can also test to create your own queries in the Optimizely online IDE using the account.
- Go to fragments folder if you are not already there: (run
pwd
to see your current folder):
cd src/graphql/fragments
-
Create and update the LandingPageBlockData.graphql file:
- Create the file:
touch LandingPageBlockData.graphql
- Add the following fragment declaration to the LandingPageBlockData.graphql file:
fragment LandingPageBlockData on LandingPageBlockData { Heading Message }
- Create the file:
-
Create and update the ContentBlock.graphql file:
- Create the file:
touch ContentBlock.graphql
- Add the following fragment declaration to the ContentBlock.graphql file:
fragment ContentBlock on ContentBlock { Title Image ImageAlignment Content }
- Create the file:
-
Create and update the ImageFile.graphql file:
- Create the file:
touch ImageFile.graphql
- Add the following fragment declaration to the ImageFile.graphql file:
fragment ImageFile on ImageFile { Thumbnail { Url } Content Url }
- Create the file:
-
Create and update the ItemsInContentArea.graphql file:
- Create the file:
touch ItemsInContentArea.graphql
- Add the following fragment declaration to the ItemsInContentArea.graphql file:
fragment ItemsInContentArea on IContent { __typename ...on ContentBlock { ...ContentBlock } ...on ImageFile { ...ImageFile } }
- Create the file:
-
Create and update the ArtistDetailsPage.graphql file:
- Create the file:
touch ArtistDetailsPage.graphql
- Add the following fragment declaration to the ArtistDetailsPage.graphql file:
fragment ArtistDetailsPage on ArtistDetailsPage { PerformanceStartTime PerformanceEndTime StageName ArtistName ArtistPhoto ArtistGenre ArtistDescription ArtistIsHeadliner Name RelativePath }
- Create the file:
-
Create and update the LandingPage.graphql file:
- Create the file:
touch LandingPage.graphql
- Add the following fragment declaration to the LandingPage.graphql file:
fragment LandingPage on LandingPage { Title Subtitle BuyTicketBlock { ...LandingPageBlockData } HeroImage MainContentArea { ContentLink { Expanded { ...ItemsInContentArea } } } FooterContentArea { ContentLink { Expanded { ...ItemsInContentArea } } } }
- Create the file:
-
Go to the graphql folder:
cd ..
-
Create and update the Start.graphql file:
- Create the file:
touch start.graphql
- Add the following query to the Start.graphql file:
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 } } } }
- Create the file:
-
Generate the TypeScript classes by tool from Optimizely Graph using the created queries.
yarn generate
-
Check the new 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 a request to Optimizely 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 go to the different parts of the site. App.tsx is called for them, and the content contains data based on which URL 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 you have not used any content yet.
-
Add bootstrap by running:
yarn add bootstrap
-
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 go to the 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;
Go to an 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 go to the different artist pages, for example:
http://localhost:3000/en/artists/la-tangerine
http://localhost:3000/en/artists/top-reindeer
Updated about 2 months ago