Как использовать Contentful с Next.js и Zod

Использование безголовых CMS с современными веб-технологиями, такими как Next.js, уже давно стало популярным способом создания веб-сайтов и управления их содержимым. В этой статье мы рассмотрим безопасный для типов и легко расширяемый метод доступа к схемам Contentful.

Цель этой статьи — создать абстракцию для каждой модели данных, которая позволяет выполнять безопасные для типов запросы. Мы назовем эту абстракцию createContentfulModel, которая будет принимать на вход схему вашего контента и предоставлять вам типизированные и проверенные данные.

Шаг 1 — Настройка рабочего пространства и клиента Contentful

Для того чтобы настроить рабочее пространство Contentful, перейдите на сайт contentful.com, зарегистрируйтесь и настройте рабочее пространство Contentful. Там вы можете сначала создать свои модели контента, а затем создать контент для этих моделей.

Возвращаясь к кодовой стороне вещей, сначала создадим новый следующий проект (с TypeScript) и установим contentful с помощью npm.

$ npx create-next-app@latest --ts
$ npm i contentful
Вход в полноэкранный режим Выход из полноэкранного режима

Мы настраиваем наш клиент Contentful в contentful/client.ts в соответствии с документацией Contentful.

import { createClient } from "contentful";

export const contentfulClient = createClient({
  space: process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID!,
  accessToken: process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN!,
});
Войти в полноэкранный режим Выход из полноэкранного режима

Мы также должны предоставить необходимые переменные окружения для настройки нашего клиента и подключения его к рабочему пространству Contentful. В Contentful, в разделе настроек и ключей API, создайте новый ключ API. Нам понадобятся его идентификатор пространства и маркер доступа к API Content Delivery. Скопируйте и вставьте нужные значения в файл .env.local, как показано ниже.

NEXT_PUBLIC_CONTENTFUL_SPACE_ID="your-space-id"
NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN="your-access-token"
Вход в полноэкранный режим Выйдите из полноэкранного режима

Шаг 2 — Настройка схем Zod для обеспечения безопасности типов

Сначала запустите npm i zod, чтобы установить Zod, пожалуй, лучшую библиотеку проверки типов, доступную для TypeScript.

Прежде чем приступить к настройке схем и форм для нашего контента, нам понадобятся типы, специфичные для Contentful. Все запросы Contentful возвращают записи объектов, которые содержат все содержимое в свойстве fields, но также содержат свойства metadata и sys, которые включают полезные метаданные об объекте, такие как его ID и метки времени.

Сначала мы создадим схему contentfulEntrySchema в contentful/contentful-entry-schema.ts, которая будет содержать схему Zod, соответствующую записи объекта с пустым свойством fields (которое мы позже расширим).

import { z } from "zod";

export const contentfulEntrySchema = z.object({
  fields: z.object({}), // Extend this later
  metadata: z.object({
    tags: z.array(z.any()),
  }),
  sys: z.object({
    space: z.object({
      sys: z.object({
        type: z.string(),
        linkType: z.string(),
        id: z.string(),
      }),
    }),
    id: z.string(),
    type: z.string(),
    createdAt: z.string(),
    updatedAt: z.string(),
    environment: z.object({
      sys: z.object({
        id: z.string(),
        type: z.string(),
        linkType: z.string(),
      }),
    }),
    revision: z.number(),
    contentType: z
      .object({
        sys: z.object({
          type: z.string(),
          linkType: z.string(),
          id: z.string(),
        }),
      })
      .optional(),
    locale: z.string(),
  }),
});
Вход в полноэкранный режим Выход из полноэкранного режима

Это позволяет полностью проверить все данные, запрашиваемые из Contentful, и мы можем легко расширить его для любого типа контента, переопределив свойство fields.

Шаг 3 — Создание createContentfulModel

Теперь, когда мы подготовили большую часть нашего шаблона, мы начнем работать над абстракцией createContentfulModel. Наша цель — создать функцию, которая принимает на вход

В качестве выхода мы хотим, чтобы функция предоставляла нам объект со следующими свойствами:

  • Схемы, которые были созданы с помощью fieldsSchemaCreator.
  • Все функции получения данных (в этом примере мы будем реализовывать только getAll) для получения всех данных без запроса.

Давайте начнем создавать функцию в contentful/create-contentful-model.ts.

// More in the next article on the create contentful model context
export type CreateContentfulModelContext = {};

export type FieldsSchemaCreator<TDataIn extends {}, TDataOut> = (
    context: CreateContentfulModelContext
) => z.Schema<TDataOut, z.ZodTypeDef, TDataIn>;

export function createContentfulModel<TDataIn extends {}, TDataOut>(
    contentType: string,
    fieldsSchemaCreator: FieldsSchemaCreator<TDataIn, TDataOut>
) {
    ...

    return { ... }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы хотим иметь возможность использовать эту функцию следующим образом:

const exampleModel = createContentfulModel("example", (ctx) => z.object({
    title: z.string(),
    description: z.string().optional(),
    rating: z.number().int().positive(),
}));

exampleModel.getAll().then(examples => {...})
Войти в полноэкранный режим Выйти из полноэкранного режима

Создатель схемы полей должен возвращать объект Zod, который представляет поля типа содержимого, к которому мы хотим получить доступ. Давайте приступим к его реализации.

export function createContentfulModel<...>(...) {

    // Set up an empty context. We'll return to this in the next article.
    const context: CreateContentfulModelContext = {};

    // Run the fieldsSchemaCreator with the context to get the type of
    // the content's fields.
    const fieldsSchema = fieldsSchemaCreator(context);

    // Using the object entry schema we defined earlier, extend its fields
    // property to define this object type's full entry schema
    const entrySchema = contentfulEntrySchema.extend({ fields: fieldsSchema });

    // Return schemas
    return {
        fieldsSchema,
        entrySchema,
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть доступ к полным схемам. Далее нам нужно создать функции извлечения данных, которые позволят нам извлекать данные всеми необходимыми способами. Примечание: для этого проекта мы будем реализовывать только функцию getAll. Остальные функции (getOneById, getAllWhere и любые другие, которые могут вам понадобиться) мы оставим на усмотрение читателя. Для небольших проектов с небольшим количеством данных, особенно с SSG, getAll может быть все, что вам нужно.

export function createContentfulModel<...>(...) {
    // ...

    // Create the get all fetcher to fetch all items of the current
    // content type.
    const getAll = async () => {
        // Fetch all items of current content type
        const res = await contentfulClient.getEntries({ content_type: contentType });

        // Parse and validate all items using zod
        const parsed = z.array(entrySchema).safeParse(res.items);

        // Handle failures
        if (!parsed.success) {
            console.error(parsed.error);
            return [];
        }

        // Return validated data with correct types
        return parsed.data;
    }

    return {
        fieldsSchema,
        entrySchema,
        getAll,
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем использовать createContentfulModel для создания типизированных, проверенных абстракций для каждого из наших типов содержимого в рабочем пространстве Contentful.

Шаг 4 — Вывод типов

Давайте еще раз воспользуемся магией Zod. Вместо того чтобы вводить тип для каждой из наших моделей содержимого, мы можем просто вывести типы из схем, которые у нас уже есть. Больше нет необходимости поддерживать представление модели контента в нашем коде в двух отдельных местах. Чтобы легко выводить типы, мы создадим types/contentful.d.ts в нашем проекте. При использовании файла .d.ts только со встроенным импортом, нам даже не нужно импортировать типы в наш проект, где они используются, нам нужно только включить contentful.d.ts в наш tsconfig.json.

Чтобы помочь в определении типов, мы создадим тип утилиты ExtractModelType, который предоставляется любой модели, созданной с помощью createContentfulModel, и возвращает тип записи контента.

// types/contentful.d.ts

// Get inner type of Array or Promise
type Inner<T> = T extends Array<infer U1>
  ? U1
  : T extends Promise<infer U2>
  ? U2
  : T;

// Helper type to extract a model's type from the model object
type ExtractModelType<
  Model extends ReturnType<
    typeof import("../contentful/create-contentful-model")["createContentfulModel"]
  >
> = Inner<Inner<ReturnType<Model["getAll"]>>>;
Вход в полноэкранный режим Выход из полноэкранного режима

Далее мы создадим пример модели контента по адресу /contentful/example-model.ts.

export const exampleModel = createContentfulModel("example", (ctx) => z.object({
    title: z.string(),
    description: z.string().optional(),
    rating: z.number().int().positive(),
}));
Вход в полноэкранный режим Выйти из полноэкранного режима

И выведем ее в types/contentful.d.ts следующим образом:

type ExampleModelEntry = ExtractModelType<
  typeof import("../contentful/example-model")["exampleModel"]
>;

type ExampleModelFields = ExampleModelEntry["fields"];
Войти в полноэкранный режим Выйти из полноэкранного режима

Тип ExampleModelEntry представляет полную запись объекта, возвращаемую Contentful в функциях fetcher. Он содержит все метаданные в полях sys и metadata. Содержимое содержится в свойстве fields и имеет тип ExampleModelFields.

Шаг 5 — Потребление моделей

После большой работы по настройке наших моделей Contentful, мы можем начать использовать их в наших приложениях. Работа, проделанная нами ранее, начинает окупаться, поскольку потреблять наши типы так же просто, как сделать следующее getStaticProps (или любое другое место, где вы можете получить данные).

import type { GetStaticProps, InferGetStaticPropsType } from 'next'
import { exampleModel } from '../contentful/example-model'

export default function Page({ examples }: InferGetStaticPropsType<typeof getStaticProps>) {
    return <ul>
        {
            examples.map(example => <li key={example.sys.id}>
                <p>{example.fields.title}</p>
                <p>{example.fields.description}</p>
                <p>{example.fields.rating} / 5</p>
            </li>)
        }
    </ul>
}

export const getStaticProps: GetStaticProps<{ examples: ExampleModelEntry[] }> = async () => {
    return {
        props: {
            examples: await exampleModel.getAll(),
        }
    }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Заключение

Об абстракциях и работе, необходимой для их создания

Если потратить время на разработку и создание хороших абстракций, ваш код будет намного легче поддерживать, читать и расширять в будущем. Работа, необходимая для создания createContentfulModel в этой статье, может показаться большой. Но подумайте о шагах, необходимых сейчас для добавления нового типа содержимого в ваше приложение.

  1. Установите новый тип содержимого в рабочем пространстве Contentful.
  2. Создайте новую модель с помощью createContentfulModel и предоставьте только схему для ее полей и id типа содержимого.
  3. Добавьте ее типы в types/contentful.d.ts.
  4. Вы готовы потреблять данные в любом месте с помощью метода .getAll().

Кроме того, абстракции делают ваш код легко расширяемым. Реализация других фетчеров, кроме фетчера getAll, сделает их доступными для всех типов контента, написав код один раз. В следующей статье мы рассмотрим расширение этой абстракции для работы с изображениями и текстом Contentful, используя объект context, о котором говорилось в этой статье.

В следующей статье

Читайте следующую статью, чтобы узнать, как мы будем расширять эту абстракцию. Мы будем создавать

  • Поддержка изображений Contentful
  • Поддержка насыщенного текста

(Еще не опубликовано)

Оцените статью
devanswers.ru
Добавить комментарий