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.
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',
},
},
],
},
],
},
],
},
],
})
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,
}
]
}
}
}
}
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>
)
}