Building a Blog
Create a posts collection, fetch content in your frontend, and let clients edit through the Admin UI.
A blog is the clearest way to see Dyrected end to end: you define a posts collection, mount the API, read posts in your frontend, and hand editing off to the Admin UI. By the end of this guide you'll have all four working together.
Prerequisites
Before starting this guide, you should have:
- A running Dyrected instance (see Quickstart)
- A frontend application (Next.js or Nuxt.js)
- Installed
@dyrected/sdkin your project
1. Define the collection
Start with the shape of a post. This collection stores the title, URL slug, body content, a draft/published status, and a publish date — and access: { read: () => true } makes published posts publicly readable:
// 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', label: 'Title', type: 'text', required: true },
{ name: 'slug', label: 'Slug', type: 'text', required: true },
{ name: 'content', label: 'Content', type: 'richText' },
{ name: 'status', label: 'Status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' },
{ name: 'publishedAt', label: 'Published At', type: 'date' },
],
},
],
})2. Mount the API
The collection needs an HTTP endpoint before your frontend can reach it. Each framework has a one-liner that wires your config into its routing:
// app/api/[...route]/route.ts
import { dyrectedNextHandler } from '@dyrected/next'
import config from '@/dyrected.config'
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = dyrectedNextHandler(config, { basePath: '' })// nuxt.config.ts
import config from './dyrected.config'
export default defineNuxtConfig({
modules: ['@dyrected/nuxt'],
dyrected: { ...config, apiBase: '/api' },
})3. Fetch the post list
With the API mounted, you can query posts from your frontend. This fetches published posts, newest first, and renders them as links. Use whichever access style fits your app — the SDK, the Nuxt composable, or a raw REST call:
// 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>
)
}Avoid blank states: You can pass initialData: fallbackPosts (imported from a local JSON file) into find(). If the database is currently empty, the SDK immediately returns this fallback data and automatically seeds the database in the background so editors can start editing it right away.
<!-- pages/blog/index.vue -->
<script setup lang="ts">
const { data } = await useDyrectedCollection('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 query = new URLSearchParams({
where: JSON.stringify({ status: { equals: 'published' } }),
sort: '-publishedAt',
})
const res = await fetch(
`${process.env.NEXT_PUBLIC_DYRECTED_URL}/api/collections/posts?${query}`,
{ 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
The post page looks up one post by its slug. Query with where on the slug, take the first result, and render its content:
// 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 query = new URLSearchParams({
where: JSON.stringify({ slug: { equals: params.slug } }),
limit: '1',
})
const res = await fetch(
`${process.env.NEXT_PUBLIC_DYRECTED_URL}/api/collections/posts?${query}`,
{ 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
Static pages won't reflect a new post until you rebuild them. Add an afterChange hook to the posts collection you defined in step 1 so publishing a post refreshes the affected pages automatically:
// 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/page.tsx. The @dyrected/next package provides a <DyrectedAdmin /> component that mounts the dashboard on that route and isolates its internal routing from the rest of your app.
// app/admin/page.tsx
import { DyrectedAdmin } from '@dyrected/next/admin'
export default function AdminPage() {
return (
<DyrectedAdmin />
)
}Adding the @dyrected/nuxt module auto-imports the <DyrectedAdmin> component, but it does not create a route for you — you decide where the dashboard lives. The admin's URL is simply the route of the page you render it in. Here it is at /admin; use a different filename (say pages/cms-admin.vue) if you want a different path.
<!-- pages/admin.vue -->
<template>
<ClientOnly>
<DyrectedAdmin />
</ClientOnly>
</template>
<script setup lang="ts">
// No import needed - DyrectedAdmin is auto-imported
definePageMeta({ layout: false })
</script>See Admin UI Overview for setup details.