You're reading docs for Nextly Alpha. APIs may change between releases.

Getting Started

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:

nextly.config.ts
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
  1. Open the admin panel at http://localhost:3000/admin
  2. Navigate to Collections in the sidebar
  3. Click Create Collection
  4. Name it Posts (slug: posts)
  5. Add fields:
    • title -- Text, required
    • slug -- Text, required, unique
    • content -- Rich Text
    • publishedAt -- Date
  6. 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 status field by hand.
  7. Click Save
  8. Repeat to create a Categories collection with fields:
    • name -- Text, required
    • slug -- 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.

  1. Go to Categories and create a few categories (e.g., "Technology", "Design")
  2. 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.

src/app/blog/page.tsx
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

src/app/blog/[slug]/page.tsx
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:

nextly.config.ts
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.

  1. In the admin panel, go to Singles
  2. Click Create Single
  3. Name it Blog Settings (slug: blog-settings)
  4. Add fields: blogTitle (Text, required), tagline (Text), postsPerPage (Number, default: 10)
  5. Click Save

Query the single in your layout:

src/app/blog/layout.tsx
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 (posts and categories) 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