Table of Contents

The purpose of this example is to demonstrate how to create and use a more complicated schema (which utilizes simple and complex object fields), write a getDocument GraphQL query, and utilize the query's result in rendering. ​

Schema

​ The schema defines a recipe collection which consists of some standard fields (name, photo, description) and more advanced fields (meta, sections). ​

// .tina/schema.ts
import { defineSchema } from '@tinacms/cli'
export default defineSchema({
  collections: [
    {
      name: 'recipe',
      label: 'Recipes',
      path: 'content/recipes',
      fields: [
        {
          name: 'datePublished',
          label: 'Published On',
          type: 'datetime',
          ui: {
            dateFormat: 'MMM D, YYYY',
          },
        },
        {
          type: 'string',
          name: 'name',
          label: 'Name',
        },
        {
          type: 'image',
          name: 'photo',
          label: 'Photo',
        },
        {
          type: 'string',
          name: 'description',
          label: 'Description',
          isBody: true,
          ui: {
            component: 'textarea',
          },
        },
        {
          type: 'object',
          name: 'meta',
          label: 'Meta',
          fields: [
            {
              name: 'prep',
              label: 'Prep Time (in minutes)',
              type: 'string',
            },
            {
              name: 'cook',
              label: 'Cook Time (in minutes)',
              type: 'string',
            },
            {
              name: 'servings',
              label: 'Servings',
              type: 'string',
            },
          ],
        },
        {
          type: 'object',
          name: 'sections',
          label: 'Sections',
          list: true,
          templates: [
            {
              name: 'ingredients',
              label: 'Ingredients',
              fields: [
                {
                  name: 'items',
                  label: 'Items',
                  type: 'string',
                  list: true,
                },
                {
                  name: 'details',
                  label: 'Details',
                  type: 'string',
                  ui: {
                    component: 'textarea',
                  },
                },
              ],
            },
            {
              name: 'directions',
              label: 'Directions',
              fields: [
                {
                  name: 'steps',
                  label: 'Steps',
                  type: 'string',
                  list: true,
                },
                {
                  name: 'details',
                  label: 'Details',
                  type: 'string',
                  ui: {
                    component: 'textarea',
                  },
                },
              ],
            },
            {
              name: 'nutrition',
              label: 'Nutrition',
              fields: [
                {
                  name: 'details',
                  label: 'Details',
                  type: 'string',
                  ui: {
                    component: 'textarea',
                  },
                },
              ],
            },
          ],
        },
      ],
    },
  ],
})

Query

​ To retrieve a single recipe, you would use a query like this one. ​ Notice that, for sections, we are using __typename to deobfuscate the available templates. While __typename can be omitted in the query, keeping it allows for comparisons later where you might want to use a different render for each template type. ​ Because meta does not have any templates, you do not need to deobfuscate and can access its properties directly. ​

// pages/recipe/[filename].tsx
import { staticRequest, gql } from 'tinacms'
export const getStaticProps = async ({ params }) => {
  const query = `
    query GetRecipeDocument($relativePath: String!) {
      getRecipeDocument(relativePath: $relativePath) {
        data {
          datePublished
          name
          photo
          description
          meta {
            prep
            cook
            servings
          }
          sections {
            __typename
            ... on RecipeSectionsIngredients {
              items
              details
            }
            ... on RecipeSectionsDirections {
              steps
              details
            }
            ... on RecipeSectionsNutrition {
              details
            }
          }
        }
      }
    }
  `

  const variables = { relativePath: `${params.filename}.md` }

  let recipe = {}
  try {
    recipe = await staticRequest({
      query,
      variables,
    })
  } catch {
    // swallow errors related to document creation
  }
  return {
    props: {
      query,
      variables,
      data: recipe,
    },
  }
}

​ This query will return an Object in the shape of: ​

{
  "data": {
    "getRecipeDocument": {
      "data": {
        "datePublished": string,
        "name": string,
        "photo": string,
        "description": string,
        "meta": {
          "prep": string,
          "cook": string,
          "servings": string,
        },
        "sections": [
          {
            "__typename": "RecipeSectionsIngredients",
            "items": [string],
            "details": string,
          },
          {
            "__typename": "RecipeSectionsDirections",
            "steps": [string],
            "details": string,
          },
          {
            "__typename": "RecipeSectionsNutrition",
            "details": string,
          }
        ]
      }
    }
  }
}

Rendering

​ The important step here is correctly retrieve the queried data out of props. The common pattern is data: { query: { data: document }}. ​ Notice that sections is iterated via map() utilizing the __typename to determine how each section should be rendered. ​

// pages/recipe/[filename].tsx
const RecipePage = (props) => {
const {
    data: {
      getRecipeDocument: { data: recipe },
    },
  }  = props;

const { datePublished, name, photo, description, meta, sections } = recipe;/**
   * Because all `datetime` fields returned by GraphQL are in UTC, we
   * need to convert them to local `datetime` using `toLocaleDateString()`.
   * This returns the date as `Jan 1, 2021`, for example.
   *
   * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleDateString
   */

  const localDatePublished = React.useMemo(() => {
  if (datePublished) {
    return new Date(datePublished).toLocaleDateString(undefined, {
      month: "short",
      day: "numeric",
      year: "numeric",
    });
   }
  }, [datePublished])return (
    <div className="recipe-page">
      {photo ? <img src={photo} alt="photo" />}
      <h1>{name}</h1>
      {localDatePublished ? <h2>{localDatePublished}</h2>}
      <p>{description}</p>
      {meta && Object.keys(meta).length > 0 && (
        <dl>
          {meta.prep && (
            <>
              <dt>Prep Time</dt>
              <dd>{meta.prep} minutes</dd>
            </>
          )}
          {meta.cook && (
            <>
              <dt>Cook Time</dt>
              <dd>{meta.cook} minutes</dd>
            </>
          )}
          {meta.servings && (
            <>
              <dt>Servings</dt>
              <dd>{meta.servings}</dd>
            </>
          )}
        </dl>
      )}
      {sections && sections.map((section) => {
        if (section.__typename === "RecipeSectionsIngredients") {
          return (
            <RecipeIngredients key={`${name}.${section.__typename}`}
            {...section} />
          )
        }
        if (section.__typename === "RecipeSectionsDirections") {
          return (
            <RecipeDirections key={`${name}.${section.__typename}`}
            {...section} />
          )
        }
        if (section.__typename === "RecipeSectionsNutrition") {
          return (
            <RecipeNutrition key={`${name}.${section.__typename}`}
            {...section} />
          )
        }
      })}
    </div>
  )
}