Интеграция Directus с фронтендом (React/Vue/Next.js)
Directus предоставляет официальный TypeScript SDK с типизированными запросами. Для Next.js App Router — Server Components для серверных запросов и React hooks для клиентского взаимодействия.
Установка SDK
npm install @directus/sdk
Настройка клиента
// lib/directus.ts
import { createDirectus, rest, staticToken, readItems, readItem, readSingleton } from '@directus/sdk'
// Типы для коллекций
interface Article {
id: number
title: string
slug: string
content: string
excerpt: string
status: 'draft' | 'published' | 'archived'
date_published: string
thumbnail: { id: string; filename_disk: string } | null
category: { id: number; name: string; slug: string } | null
author: { id: string; first_name: string; last_name: string } | null
}
interface DirectusSchema {
articles: Article[]
categories: { id: number; name: string; slug: string }[]
products: { id: number; name: string; price: number; stock: number }[]
}
const directus = createDirectus<DirectusSchema>(process.env.DIRECTUS_URL!)
.with(staticToken(process.env.DIRECTUS_STATIC_TOKEN!))
.with(rest())
export { directus }
// Вспомогательная функция для URL медиа
export function getDirectusAssetURL(fileId: string, params?: Record<string, string>) {
const url = new URL(`/assets/${fileId}`, process.env.DIRECTUS_URL)
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value))
}
return url.toString()
}
Server Components (Next.js App Router)
// app/articles/page.tsx
import { directus } from '@/lib/directus'
import { readItems } from '@directus/sdk'
export default async function ArticlesPage({
searchParams,
}: {
searchParams: { page?: string; category?: string }
}) {
const page = Number(searchParams.page) || 1
const limit = 12
const articles = await directus.request(
readItems('articles', {
fields: ['id', 'title', 'slug', 'excerpt', 'date_published',
{ thumbnail: ['id', 'filename_disk', 'width', 'height'] },
{ category: ['name', 'slug'] },
],
filter: {
status: { _eq: 'published' },
...(searchParams.category ? { category: { slug: { _eq: searchParams.category } } } : {}),
},
sort: ['-date_published'],
limit,
offset: (page - 1) * limit,
})
)
return <ArticleGrid articles={articles} />
}
Динамические маршруты с ISR
// app/articles/[slug]/page.tsx
import { directus, getDirectusAssetURL } from '@/lib/directus'
import { readItems } from '@directus/sdk'
import { notFound } from 'next/navigation'
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const articles = await directus.request(
readItems('articles', {
fields: ['*', { thumbnail: ['*'] }, { author: ['first_name', 'last_name'] }],
filter: {
slug: { _eq: params.slug },
status: { _eq: 'published' },
},
limit: 1,
})
)
const article = articles[0]
if (!article) notFound()
return (
<article>
{article.thumbnail && (
<img
src={getDirectusAssetURL(article.thumbnail.id, { width: '1200', quality: '80' })}
alt={article.title}
/>
)}
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.content }} />
</article>
)
}
export async function generateStaticParams() {
const articles = await directus.request(
readItems('articles', {
fields: ['slug'],
filter: { status: { _eq: 'published' } },
limit: -1,
})
)
return articles.map(a => ({ slug: a.slug }))
}
export const revalidate = 3600
On-demand Revalidation
// app/api/revalidate/route.ts — принимает вебхук Directus
import { revalidateTag, revalidatePath } from 'next/cache'
export async function POST(req: Request) {
const secret = req.headers.get('x-webhook-secret')
if (secret !== process.env.DIRECTUS_WEBHOOK_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const { collection, keys, payload } = await req.json()
revalidateTag(collection)
if (collection === 'articles' && payload?.slug) {
revalidatePath(`/articles/${payload.slug}`)
}
return Response.json({ revalidated: true })
}
Клиентский React хук
// hooks/useDirectus.ts
'use client'
import { useState, useEffect } from 'react'
export function useDirectusItems<T>(
collection: string,
query: Record<string, any>
): { data: T[]; loading: boolean; error: Error | null } {
const [data, setData] = useState<T[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
const params = new URLSearchParams({ query: JSON.stringify(query) })
fetch(`/api/directus/${collection}?${params}`)
.then(r => r.json())
.then(d => setData(d.data || []))
.catch(setError)
.finally(() => setLoading(false))
}, [collection, JSON.stringify(query)])
return { data, loading, error }
}
Directus Realtime (WebSockets)
// Подписка на изменения в реальном времени
import { createDirectus, realtime } from '@directus/sdk'
const client = createDirectus(DIRECTUS_URL).with(realtime())
await client.connect()
const subscription = await client.subscribe('articles', {
event: 'update',
query: { fields: ['id', 'title', 'status'] },
})
for await (const message of subscription) {
console.log('Article updated:', message.data)
}
Сроки
Интеграция Directus с Next.js (SDK, ISR, вебхуки, TypeScript типы) — 2–3 дня.







