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

Getting Started

Quick Start: Build a Blog

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, generate a migration from the change, commit it, and apply it where you deploy:

pnpm nextly migrate:create add_posts_and_categories
pnpm nextly migrate

nextly migrate applies the files in migrations/; it does not create tables from nextly.config.ts on its own. See Production migrations.

  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. Get the instance with getNextly({ config }): it starts Nextly in this process if nothing has yet, so the page works on a cold server too.

src/app/blog/page.tsx
import nextlyConfig from "../../../nextly.config";
import { getNextly } from "nextly";

export default async function BlogPage() {
  const nextly = await getNextly({ config: nextlyConfig });
  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 nextlyConfig from "../../../../nextly.config";
import { getNextly } from "nextly";
import { notFound } from "next/navigation";

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;

  const nextly = await getNextly({ config: nextlyConfig });
  const { items } = await nextly.find({
    collection: "posts",
    where: {
      slug: { equals: slug },
      status: { equals: "published" },
    },
    // Rich text is stored as JSON; ask for HTML to render it directly.
    richTextFormat: "html",
    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",
  label: { 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:create <name>, commit the file, then run pnpm nextly migrate where you deploy.

  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 nextlyConfig from "../../../nextly.config";
import { getNextly } from "nextly";

export default async function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const nextly = await getNextly({ config: nextlyConfig });
  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

You now 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