HomeDev guideAPI ReferenceGraphQL
Dev guideUser GuideGitHubNuGetDev CommunitySubmit a ticketLog In
GitHubNuGetDev CommunitySubmit a ticket

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

  1. This tutorial assumes you have yarn already installed. You can install yarn through npm:
    npm install --global yarn
    
  2. Set up the Music Festival CMS backend site.
  3. Add the package "Optimizely.ContentGraph.Cms" to the site.
  4. Configure keys to "Optimizely Graph" for the site
  5. Run the scheduled job "Optimizely Graph" to synchronize all content types and content from the site to "Optimizely Graph".

Tutorial

Set up your CMS site

  1. Create your site by running
yarn create react-app music-festival-graphql --template typescript
  1. Go to to the new folder for your created site
cd music-festival-graphql
  1. Start your site by running
yarn start
  1. Navigate to your new site by going http://localhost:3000 in your web browser.
  2. Install @tanstack/react-query by running:
yarn add @tanstack/react-query@4
  1. Install html-react-parser by running:
yarn add html-react-parser
  1. 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
  1. Create a codegen.yaml file to root by running
touch codegen.yaml
  1. 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 preparations section).

  1. Update the package.json file by adding the following inside "scripts": "generate": "graphql-codegen"
    Your 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"
    ]
  }
}
  1. 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.

Add GraphQL files

  1. Create folders for graphql and fragments under src by running:

    cd src
    mkdir graphql
    cd graphql
    mkdir fragments
    
  2. 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.

  1. Navigate to fragments folder, if you are not already there (run pwd to see your current folder):
cd src/graphql/fragments
  1. Create and update the LandingPageBlockData.graphql file:

    1. Create the file:
      touch LandingPageBlockData.graphql
      
    2. Add the following fragment declaration to the LandingPageBlockData.graphql file:
      fragment LandingPageBlockData on LandingPageBlockData {
          Heading
          Message
      }
      
  2. Create and update the ContentBlock.graphql file:

    1. Create the file:
      touch ContentBlock.graphql
      
    2. Add the following fragment declaration to the ContentBlock.graphql file:
      fragment ContentBlock on ContentBlock {
          Title
          Image
          ImageAlignment
          Content
      }
      
  3. Create and update the ImageFile.graphql file:

    1. Create the file:
      touch ImageFile.graphql
      
    2. Add the following fragment declaration to the ImageFile.graphql file:
      fragment ImageFile on ImageFile {
          Thumbnail {
              Url
          }
          Content
          Url
      }
      
  4. Create and update the ItemsInContentArea.graphql file:

    1. Create the file:
      touch ItemsInContentArea.graphql
      
    2. Add the following fragment declaration to the ItemsInContentArea.graphql file:
      fragment ItemsInContentArea on IContent {
          __typename
          ...on ContentBlock {
              ...ContentBlock
          }
          ...on ImageFile {
              ...ImageFile
          }
      }
      
  5. Create and update the ArtistDetailsPage.graphql file:

    1. Create the file:
         touch ArtistDetailsPage.graphql
      
    2. Add the following fragment declaration to the ArtistDetailsPage.graphql file:
      fragment ArtistDetailsPage on ArtistDetailsPage {
          PerformanceStartTime
          PerformanceEndTime
          StageName
          ArtistName
          ArtistPhoto
          ArtistGenre
          ArtistDescription
          ArtistIsHeadliner
          Name
          RelativePath
      }
      
  6. Create and update the LandingPage.graphql file:

    1. Create the file:
      touch LandingPage.graphql
      
    2. 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
                  }
              }
          }
      }
      
  7. Navigate to the graphql folder:

    cd ..
    
  8. Create and update the Start.graphql file:

    1. Create the file:
      touch start.graphql
      
    2. 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
                  }
              }
          }
      }
      
  9. Generate the TypeScript classes by tool from Optimizely Graph using the created queries.

    yarn generate
    
  10. 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.

Step 3. Implement the site

Create a starting point for the site, where you make request to Optimizely Graph with the Start.graphql query.

  1. 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();
    
  2. 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;
    

Step 4. 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.

  1. Build the site with the yarn build command.

  2. 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.
  3. Add bootstrap by running:

    yarn add bootstrap
    
  4. 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

  5. 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.

  1. 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