Dyrected
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

'use client'
import { AdminUI } from '@dyrected/admin'
import { useRouter } from 'next/navigation'
import '@dyrected/admin/dist/index.css'

export default function AdminPage() {
  const router = useRouter()
  return (
    <AdminUI
      baseUrl={process.env.NEXT_PUBLIC_DYRECTED_URL!}
      apiKey={process.env.NEXT_PUBLIC_DYRECTED_API_KEY!}
      basename="/admin"
      onNavigate={(path) => router.push(`/admin${path}`)}
    />
  )
}

The Admin UI mounts automatically at /admin when you add the @dyrected/nuxt module. Configure the path in nuxt.config.ts:

dyrected: {
  ...config,
  adminPath: '/admin',
}

See Admin UI Overview for setup details.

On this page