This sample app showcase Kontent.ai GraphQL API endpoint usage in combination with React.js using Apollo client.
In this section you can find how to get the application ready in development mode.
- Node.js (LTS recommended)
In the project directory, install all dependencies and run the development environment.
npm install
npm start
🎉 Open http://localhost:3000 with your browser to see the result.
This optional section allows you to create your own copy of the project in Kontent.ai so that you can make changes. If you skip this step, the application is connected to the published data of a shared project that is read-only via API for you.
-
Create an account on Kontent.ai
-
After signing up, create an empty project.
-
Go to the "Project Settings", select API keys and copy the following keys for further reference
- Project ID
- Management API key
-
Use the Kontent.ai Backup Manager and import data to the newly created project from
kontent-backup.zip
file via command line:npm i -g @kentico/kontent-backup-manager@3.0.1 # or yarn global add @kentico/kontent-backup-manager@3.0.1 kbm --action=restore --projectId=<Project ID> --apiKey=<Management API key> --zipFilename=kontent-backup
-
Go to your Kontent.ai project and publish the imported items.
-
Set up environment variables
-
Copy the
.env.template
file in this directory to.env
(which will be ignored by Git):cp .env.template .env
-
-
Run the development server
npm run start
🎉 Open http://localhost:3000 with your browser to see the result.
By default, the content is loaded from a shared Kontent.ai project. If you want to use your own clone of the project so that you can customize it and experiment with Kontent, continue to the next section.
Variable | Required | Description |
---|---|---|
REACT_APP_KONTENT_PROJECT_ID | NO | Project identification |
REACT_APP_KONTENT_GRAPHQL_ENDPOINT | NO | Kontent.ai GraphQL endpoint |
REACT_APP_KONTENT_PREVIEW_API_KEY | NO | Preview API key to retrieve unpublished content. If set, the application is fetching unpublished content, if not published content is being fetched. |
REACT_APP_GA_ANALYTICS_TOKEN | NO | If you want to inject Google analytics script |
Run the development server:
npm start
🎉 Open http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying content in Kontent.ai project. The page auto-updates as you edit the content, but you need to publish the changes in order to see them on site.
npm start
- Runs the app in the development mode. Open http://localhost:3000 to view it in the browser.npm test
- Launches the test runner in the interactive watch mode. See the section about running tests for more information.npm build
- Builds the app for production to thebuild
folder. It correctly bundles React in production mode and optimizes the build for the best performance. See the section about deployment for more information.npm eject
- Note: this is a one-way operation. Once youeject
, you can’t go back! If you aren’t satisfied with the build tool and configuration choices, you caneject
at any time. This command will remove the single build dependency from your project.
This section describes the content model of the site and the use cases that are supposed to demonstrate GraphQL capabilities.
The site is using a simple layout. A header with a Logo and a menu. Depending on the type of the "Content" you can have one of the layouts:
- Landing page - List of various sections displayed as rows.
- Listing page - A grid of homogenous content pieces - specifically articles in the sample.
- Simple page - A simple page rendering the title, subtitle, image, and rich text element.
This data is stored in Homepage
content item containing information about SEO, sitemap, layout (menu, title, copyright), and logo.
This data loaded in the App component as part of the query. Simplified version of the query could be seen below:
{
homepage(codename: "home_page") {
title
favicon {
url
}
font {
_system_ {
codename
}
}
palette {
_system_ {
codename
}
}
}
}
Element subpages
of Homepage
, prepared for Web Spotlight feature, is used as a root of the Sitemap structure. It is a hierarchical structure, that allows mapping URLs items in the sitemap to content items and vice-versa.
To achieve mapping "URL"<->"Navigation Item", there is a content type named Navigation Item
that could be linked hierarchically (since it is also containing subpages
element). This data (mappings
property) is then used across tha application for link resolution.
Hierarchical Sitemap is showcased under /about-us/more-information
.
There are also
Listing pages
showcased on/blog
listing page containing detail pages/blog/<POST-URL-SLUG>
i.e./blog/5-tips-to-solve-your-problems
. This registration is based inListing page
(described in Content model section). Listing page specifies by thecontent_type
text field what content items should be registered under its route (expecting this type has URL slug set) - more information in Listing page section.
Simplified version of the data loading:
{
homepage(codename: "home_page") {
subpages {
items {
... on NavigationItem {
...SubpageNavigationItemFields # see ~/src/graphQLFragments.js
subpages {
items {
...SubpageNavigationItemFields # see ~/src/graphQLFragments.js
}
}
}
}
}
}
}
The menu is modeled using main_menu
linked items element of the Homepage
that contains the item with single-level list of menu items linked in actions
element.
The data loaded in the App component are then rendered in the Header component.
Simplified version of the data loading:
{
homepage(codename: "home_page") {
mainMenu(limit: 1) {
items {
... on Menu {
_system_ {
codename
}
actions {
items {
... on Action {
...ActionFields # see ~/src/graphQLFragments.js
}
}
}
}
}
}
}
}
Rich text resolution itself depends on technology you are using. But Rich text element itself is providing all data for the resolution.
Following sections showcase what properties you could use to load this data. All of these resolutions is showcases in /style-guide
simple page.
{
simplePage(codename: "style_guide") {
content {
# Rich text element
html
components {
items {
_system_ {
id
codename
type {
_system_ {
codename
}
}
}
... on Quote {
quoteText
}
... on CodeBlock {
code {
html
}
}
}
}
}
}
}
{
simplePage(codename: "style_guide") {
content {
# Rich text element
html
modularContent {
items {
_system_ {
id
codename
type {
_system_ {
codename
}
}
}
... on Quote {
quoteText
}
... on CodeBlock {
code {
html
}
}
}
}
}
}
}
{
simplePage(codename: "style_guide") {
content {
# Rich text element
html
links {
items {
... on Quote {
quoteText
}
... on CodeBlock {
code {
html
}
}
}
}
}
}
}
{
simplePage(codename: "style_guide") {
content {
# Rich text element
html
assets {
url
name
description
imageId
}
}
}
}
Landing page itself showcases a resolution of different content types linked in single linked items element.
Landing page is using linked items element called sections
.
In this use case it is a list of section and application is using system.type
to distinguish what component from ~/src/component/sections
to render. The logic is in ~src/LandingPage.js
.
{
landingPage(codename: "main_content") {
sections {
items {
_system_ {
codename
type {
_system_ {
codename
}
}
}
... on HeroSection {
image {
...AssetFields # see ~/src/graphQLFragments.js
}
title
content {
...RichTextFields # see ~/src/graphQLFragments.js
}
actions {
items {
...ActionFields # see ~/src/graphQLFragments.js
}
}
}
... on FeaturesSection {
title
subtitle {
...RichTextFields # see ~/src/graphQLFragments.js
}
features {
items {
... on Feature {
image {
...AssetFields # see ~/src/graphQLFragments.js
}
title
content {
...RichTextFields # see ~/src/graphQLFragments.js
}
actions {
items {
...ActionFields # see ~/src/graphQLFragments.js
}
}
}
}
}
}
... on CtaSection {
title
subtitle {
...RichTextFields # see ~/src/graphQLFragments.js
}
action {
items {
...ActionFields # see ~/src/graphQLFragments.js
}
}
}
... on ListingSection {
title
subtitle {
...RichTextFields # see ~/src/graphQLFragments.js
}
orderBy
contentType
numberOfItems
}
}
}
}
}
Listing page showcase a various features you can stumble upon if you want to list content items with possibility to provide a detail.
The core it so to implement the list with detail pages.
What to list is defined in the Listing page
content type by the content_type
test element specifying, what types you want to list. It is possible to extend the use case, to define multiple types and limit/filter them somehow.
The implementation of the listing page is stored in src/ListingPage.js
.
{
post_All {
# Strongly typed collections of items based on `Post`content type
items {
_system_ {
type {
_system_ {
codename
}
}
codename
}
image {
...AssetFields
}
title
slug
excerpt
publishingDate
author {
... on Author {
firstName
lastName
}
}
}
}
}
Detail page of the post is implemented in ~src/Post.js
:
query PostPageQuery($codename: String!) {
post(codename: $codename) {
seo {
...SeoFields # see ~/src/graphQLFragments.js
}
_system_ {
type {
_system_ {
codename
}
}
}
image {
...AssetFields # see ~/src/graphQLFragments.js
}
title
publishingDate
author(limit: 1) {
items {
... on Author {
firstName
lastName
}
}
}
subtitle
content {
...RichTextFields # see ~/src/graphQLFragments.js
}
}
}
Paging information is provided by query string parameter page
, so if you want a second page the URL would be /blog?page=2
. The size of the page is hardcoded to 3
, but could be configurable as well as the pace number.
In GraphQl, you just use a filter in the query:
query PostsQuery($limit: Int, $offset: Int) {
post_All(limit: $limit, offset: $offset) {
# Strongly typed collections of items based on `Post`content type
items {
# ...
}
}
}
You can extend the posts query setting where
parameter in query.
query PostsQuery($author: String) {
post_All(where: {author: {containsAny: [$author]}}) {
# Strongly typed collections of items based on `Post`content type
items {
# ...
}
}
}
query PostsQuery($persona: String) {
post_All(where: {persona: {containsAny: [$persona]}}) {
# Strongly typed collections of items based on `Post`content type
items {
# ...
}
}
}
query PostsQuery($persona: String) {
post_All(where: {
AND: [ # It is also possible to use OR and create more complex filter queries
{ persona: {containsAny: [$persona]} },
{ author: {containsAny: [$author]} }
]
}) {
# Strongly typed collections of items based on `Post`content type
items {
# ...
}
}
}
The package is including tracking header to the requests to Kontent, which helps to identify the adoption of the source plugin and helps to analyze what happened in case of error. If you think that tracking should be optional feel free to raise the feature or pull request.
This project was bootstrapped with Create React App.
You can learn more in the Create React App documentation.
To learn React, check out the React documentation.