Skip to content

A landing page from blocks ​

What you'll build

A pages collection whose content is a list of blocks (hero, features, call to action) that editors add, reorder and fill in, and a page that renders them. Uses: blocks.

1. The collection ​

ts
{
  slug: 'pages',
  useAsTitle: 'title',
  drafts: true,
  preview: ({ doc }) => `/${doc.slug ?? ''}`,
  access: { read: ({ user }) => (user ? true : { status: { equals: 'published' } }) },
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'slug', type: 'slug', from: 'title' },
    {
      name: 'layout',
      type: 'blocks',
      blocks: [
        {
          slug: 'hero',
          labels: { singular: { en: 'Hero', th: 'ส่วนหัว' } },
          fields: [
            { name: 'heading', type: 'text', required: true },
            { name: 'text', type: 'textarea' },
            { name: 'image', type: 'upload' },
          ],
        },
        {
          slug: 'features',
          labels: { singular: { en: 'Features', th: 'จุดเด่น' } },
          fields: [
            {
              name: 'items',
              type: 'array',
              maxRows: 6,
              fields: [
                { name: 'title', type: 'text', required: true },
                { name: 'text', type: 'textarea' },
              ],
            },
          ],
        },
        {
          slug: 'cta',
          labels: { singular: { en: 'Call to action', th: 'ปุ่มชวนคลิก' } },
          fields: [
            { name: 'text', type: 'text', required: true },
            { name: 'buttonLabel', type: 'text', required: true },
            { name: 'buttonUrl', type: 'text', required: true },
          ],
        },
      ],
    },
  ],
}

In the admin, Add block offers the three kinds; rows can be dragged into any order.

2. Render it ​

Switch on blockType; TypeScript narrows each row to its block's fields.

vue
<template>
  <template v-for="block in page.layout" :key="block.id">
    <section v-if="block.blockType === 'hero'" class="hero">
      <h1>{{ block.heading }}</h1>
      <p>{{ block.text }}</p>
    </section>
    <section v-else-if="block.blockType === 'features'" class="features">
      <div v-for="item in block.items" :key="item.id">
        <h3>{{ item.title }}</h3>
        <p>{{ item.text }}</p>
      </div>
    </section>
    <section v-else-if="block.blockType === 'cta'" class="cta">
      <p>{{ block.text }}</p>
      <a :href="block.buttonUrl">{{ block.buttonLabel }}</a>
    </section>
  </template>
</template>
tsx
export function Blocks({ layout }: { layout: Page['layout'] }) {
  return layout?.map((block) => {
    switch (block.blockType) {
      case 'hero':
        return <section key={block.id}><h1>{block.heading}</h1><p>{block.text}</p></section>
      case 'features':
        return (
          <section key={block.id}>
            {block.items?.map((item) => <div key={item.id}><h3>{item.title}</h3><p>{item.text}</p></div>)}
          </section>
        )
      case 'cta':
        return <section key={block.id}><p>{block.text}</p><a href={block.buttonUrl}>{block.buttonLabel}</a></section>
    }
  })
}

Page is CollectionDocument<typeof config, 'pages'>; see TypeScript.

3. Query inside blocks ​

ts
// Pages that have a call to action
await cms.find('pages', { where: { 'layout.blockType': { equals: 'cta' } } })

Released under the MIT License.