Использование безголовых 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
в этой статье, может показаться большой. Но подумайте о шагах, необходимых сейчас для добавления нового типа содержимого в ваше приложение.
- Установите новый тип содержимого в рабочем пространстве Contentful.
- Создайте новую модель с помощью
createContentfulModel
и предоставьте только схему для ее полей и id типа содержимого. - Добавьте ее типы в
types/contentful.d.ts
. - Вы готовы потреблять данные в любом месте с помощью метода
.getAll()
.
Кроме того, абстракции делают ваш код легко расширяемым. Реализация других фетчеров, кроме фетчера getAll
, сделает их доступными для всех типов контента, написав код один раз. В следующей статье мы рассмотрим расширение этой абстракции для работы с изображениями и текстом Contentful, используя объект context
, о котором говорилось в этой статье.
В следующей статье
Читайте следующую статью, чтобы узнать, как мы будем расширять эту абстракцию. Мы будем создавать
- Поддержка изображений Contentful
- Поддержка насыщенного текста
(Еще не опубликовано)