Quick Start: Build a Blog in 5 Minutes
Create a blog with posts and categories using Nextly. Define content with code or build it visually.
This guide walks you through building a blog with posts and categories. You will define a content model, run migrations, create content in the admin panel, and query it in a Next.js page.
Prerequisites: Complete the Installation steps first, including creating the super admin at /admin/setup.
1. Define Your Collections
Open nextly.config.ts and define a posts collection and a categories collection:
import {
defineConfig,
defineCollection,
text,
richText,
relationship,
date,
} from "nextly/config";
const posts = defineCollection({
slug: "posts",
labels: { singular: "Post", plural: "Posts" },
fields: [
text({ name: "title", required: true }),
text({ name: "slug", required: true, unique: true }),
richText({ name: "content" }),
relationship({ name: "category", relationTo: "categories" }),
date({ name: "publishedAt" }),
],
// Built-in Draft/Published lifecycle: injects a NOT NULL `status`
// system column (default 'draft') and adds Save Draft / Publish
// buttons in the admin entry editor.
status: true,
});
const categories = defineCollection({
slug: "categories",
labels: { singular: "Category", plural: "Categories" },
fields: [
text({ name: "name", required: true }),
text({ name: "slug", required: true, unique: true }),
],
});
export default defineConfig({
collections: [posts, categories],
singles: [],
typescript: {
outputFile: "./src/types/generated/nextly-types.ts",
},
});In development, Nextly auto-syncs schema changes when you save nextly.config.ts. For production, run:
pnpm nextly migrate- Open the admin panel at http://localhost:3000/admin
- Navigate to Collections in the sidebar
- Click Create Collection
- Name it
Posts(slug:posts) - Add fields:
title-- Text, requiredslug-- Text, required, uniquecontent-- Rich TextpublishedAt-- Date
- Open the Advanced tab on the collection and toggle Status (Draft / Published) on. This adds the system status column without you having to declare a
statusfield by hand. - Click Save
- Repeat to create a
Categoriescollection with fields:name-- Text, requiredslug-- Text, required, unique
The Visual Schema Builder creates the database tables automatically. No migration step needed in development.
2. Add Content
Open the admin panel at http://localhost:3000/admin.
- Go to Categories and create a few categories (e.g., "Technology", "Design")
- Go to Posts and create a post:
- Set a title and slug
- Write some content in the rich text editor
- Select a category
- Set status to "Published"
- Set a publish date
3. Query Content in Your App
Use the Direct API to fetch posts in a Server Component. The Direct API runs on the server -- no HTTP requests, no REST endpoints needed.
import { nextly } from "nextly";
export default async function BlogPage() {
const { items: posts } = await nextly.find({
collection: "posts",
where: {
status: { equals: "published" },
},
sort: "-publishedAt",
limit: 10,
});
return (
<div>
<h1>Blog</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</article>
))}
</div>
);
}Fetch a Single Post
import { nextly } from "nextly";
import { notFound } from "next/navigation";
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const { items } = await nextly.find({
collection: "posts",
where: {
slug: { equals: slug },
status: { equals: "published" },
},
limit: 1,
});
const post = items[0];
if (!post) notFound();
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}4. Add a Single
Singles are perfect for site-wide settings. Add a blog settings single to your config:
import { defineSingle, text, number } from "nextly/config";
const blogSettings = defineSingle({
slug: "blog-settings",
labels: { singular: "Blog Settings" },
fields: [
text({ name: "blogTitle", required: true, label: "Blog Title" }),
text({ name: "tagline", label: "Tagline" }),
number({ name: "postsPerPage", defaultValue: 10 }),
],
});
export default defineConfig({
collections: [posts, categories],
singles: [blogSettings],
// ... rest of config
});In development, save nextly.config.ts and the schema syncs automatically. For production, run pnpm nextly migrate.
- In the admin panel, go to Singles
- Click Create Single
- Name it
Blog Settings(slug:blog-settings) - Add fields:
blogTitle(Text, required),tagline(Text),postsPerPage(Number, default: 10) - Click Save
Query the single in your layout:
import { nextly } from "nextly";
export default async function BlogLayout({
children,
}: {
children: React.ReactNode;
}) {
const settings = await nextly.findSingle({
slug: "blog-settings",
});
return (
<div>
<header>
<h1>{settings?.blogTitle}</h1>
<p>{settings?.tagline}</p>
</header>
{children}
</div>
);
}What You Built
In 5 minutes, you have:
- Two collections (
postsandcategories) with typed fields - A single (
blog-settings) for site-wide configuration - An admin panel to manage all content at
/admin - Server-rendered pages that query content directly with the Direct API
Next Steps
- Project Structure -- Understand how the project is organised
- Fields -- Explore all available field types
- Direct API -- Full API reference for querying content
- Media Storage -- Set up image and file uploads