HomeDev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideGitHubNuGetDev CommunityDoc feedbackLog In

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

  1. Setting up the Music Festival CMS backend site (https://github.com/episerver/content-delivery-js-sdk/tree/master/samples/music-festival-vue-decoupled/backend).
  2. 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
  3. 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
  4. 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

  1. Create the site with the yarn create react-app music-festival-graphql --template typescript command.

  2. Go to the folder for the created site with the cd music-festival-graphql command.

  3. Start the site with the yarn start command.

  4. Go to http://localhost:3000.

  5. Install @tanstack/react-query with the yarn add @tanstack/react-query command.

  6. Install html-react-parser with the yarn add html-react-parser command.

  7. 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
    
  8. Add codegen.yaml to root with the touch codegen.yaml command.

  9. 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).

  10. 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"
        ]
      }
    }
    
  11. 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.

  12. Create folders for graphql under src with the mkdir src/graphql/fragments command.

  13. 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. Go to fragments folder with the cd src/graphql/fragments command.

  2. Create LandingPageBlockData.graphql with the touch LandingPageBlockData.graphql command.

    fragment LandingPageBlockData on LandingPageBlockData {
        Heading
        Message
    }
    
  3. Create ContentBlock.graphql with the touch ContentBlock.graphql command.

    fragment ContentBlock on ContentBlock {
        Title
        Image
        ImageAlignment
        Content
    }
    
  4. Create ImageFile.graphql with the touch ImageFile.graphql command.

    fragment ImageFile on ImageFile {
        Thumbnail {
            Url
        }
        Content
        Url
    }
    
  5. Create ItemsInContentArea.graphql with the touch ItemsInContentArea.graphql command.

    fragment ItemsInContentArea on IContent {
        __typename
        ...on ContentBlock {
            ...ContentBlock
        }
        ...on ImageFile {
            ...ImageFile
        }
    }
    
  6. Create ArtistDetailsPage.graphql with the touch ArtistDetailsPage.graphql command.

    fragment ArtistDetailsPage on ArtistDetailsPage {
        PerformanceStartTime
        PerformanceEndTime
        StageName
        ArtistName
        ArtistPhoto
        ArtistGenre
        ArtistDescription
        ArtistIsHeadliner
        Name
        RelativePath
    }
    
  7. 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
                }
            }
        }
    }
    
  8. Go to graphql folder with the cd.. command.

  9. 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
                }
            }
        }
    }
    
  10. Run the yarn generate command to generate TypeScript classes by tool from Content Graph using the created queries (update gerated.ts).

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

  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;
    

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 with the yarn add bootstrap command.

  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