Guides
Building a Blog
Create a posts collection, fetch content in your frontend, and let clients edit through the Admin UI.
1. Define the collection
// dyrected.config.ts
import { defineConfig } from '@dyrected/core'
import { SqliteAdapter } from '@dyrected/db-sqlite'
export default defineConfig({
db: new SqliteAdapter({ filename: './dyrected.db' }),
collections: [
{
slug: 'posts',
access: { read: () => true },
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', required: true },
{ name: 'content', type: 'richtext' },
{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' },
{ name: 'publishedAt', type: 'date' },
],
},
],
})2. Mount the API
// app/dyrected/[...route]/route.ts
import { createDyrectedApp } from '@dyrected/core/server'
import config from '@/dyrected.config'
const app = createDyrectedApp(config)
export const GET = app.fetch
export const POST = app.fetch
export const PATCH = app.fetch
export const DELETE = app.fetch// nuxt.config.ts
import config from './dyrected.config'
export default defineNuxtConfig({
modules: ['@dyrected/nuxt'],
dyrected: { ...config, apiBase: '/dyrected' },
})3. Fetch the post list
// app/blog/page.tsx
import { createClient } from '@dyrected/sdk'
const client = createClient({
baseUrl: process.env.NEXT_PUBLIC_DYRECTED_URL!,
apiKey: process.env.DYRECTED_API_KEY!,
})
export default async function BlogPage() {
const { docs: posts } = await client.collection('posts').find({
where: { status: { equals: 'published' } },
sort: '-publishedAt',
})
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
)
}<!-- pages/blog/index.vue -->
<script setup lang="ts">
const { data } = await useDyrectedFind('posts', {
where: { status: { equals: 'published' } },
sort: '-publishedAt',
})
</script>
<template>
<ul>
<li v-for="post in data?.docs" :key="post.id">
<NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>
</li>
</ul>
</template>// app/blog/page.tsx
export default async function BlogPage() {
const res = await fetch(
`${process.env.NEXT_PUBLIC_DYRECTED_URL}/collections/posts?where[status][equals]=published&sort=-publishedAt`,
{ headers: { 'x-api-key': process.env.DYRECTED_API_KEY! } }
)
const { docs } = await res.json()
return (
<ul>
{docs.map((post: any) => (
<li key={post.id}><a href={`/blog/${post.slug}`}>{post.title}</a></li>
))}
</ul>
)
}4. Fetch a single post
// app/blog/[slug]/page.tsx
import { createClient } from '@dyrected/sdk'
const client = createClient({
baseUrl: process.env.NEXT_PUBLIC_DYRECTED_URL!,
apiKey: process.env.DYRECTED_API_KEY!,
})
export default async function PostPage({ params }: { params: { slug: string } }) {
const { docs } = await client.collection('posts').find({
where: { slug: { equals: params.slug } },
limit: 1,
})
const post = docs[0]
if (!post) return <div>Not found</div>
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useDyrectedDoc('posts', route.params.slug as string)
</script>
<template>
<article v-if="post">
<h1>{{ post.title }}</h1>
<div v-html="post.content" />
</article>
<div v-else>Not found</div>
</template>// app/blog/[slug]/page.tsx
export default async function PostPage({ params }: { params: { slug: string } }) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_DYRECTED_URL}/collections/posts?where[slug][equals]=${params.slug}&limit=1`,
{ headers: { 'x-api-key': process.env.DYRECTED_API_KEY! } }
)
const { docs } = await res.json()
const post = docs[0]
if (!post) return <div>Not found</div>
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}5. Revalidate on publish
// dyrected.config.ts
import { revalidatePath } from 'next/cache'
hooks: {
afterChange: [
async ({ doc }) => {
if (doc.status === 'published') {
revalidatePath('/blog')
revalidatePath(`/blog/${doc.slug}`)
}
},
],
}// dyrected.config.ts
hooks: {
afterChange: [
async ({ doc }) => {
if (doc.status === 'published') {
await $fetch('/api/revalidate', {
method: 'POST',
body: { slug: doc.slug },
})
}
},
],
}6. Embed the Admin UI
Create a catch-all page at app/admin/[[...route]]/page.tsx. The @dyrected/next package provides a <DyrectedAdmin /> component that handles all the mounting and routing isolation for you.
// app/admin/[[...route]]/page.tsx
import { DyrectedAdmin } from '@dyrected/next/admin'
export default function AdminPage() {
return (
<DyrectedAdmin basename="/admin" />
)
}The Admin UI mounts automatically at /admin when you add the @dyrected/nuxt module. You can also create a custom admin page if you want to use a different route (like /cms-admin):
<!-- pages/cms-admin.vue -->
<template>
<ClientOnly>
<DyrectedAdmin basename="/cms-admin" />
</ClientOnly>
</template>
<script setup lang="ts">
// No import needed - DyrectedAdmin is auto-imported
definePageMeta({ layout: false })
</script>See Admin UI Overview for setup details.