# 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](https://nextlyhq.com/docs/getting-started/installation.md) steps first, including creating the super admin at `/admin/setup`.

## 1. Define Your Collections

**Code-First**

Open `nextly.config.ts` and define a `posts` collection and a `categories` collection:

```typescript title="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:

```bash
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](https://nextlyhq.com/docs/guides/production-migrations.md).

**Visual Schema Builder**

1. Open the admin panel at [http\://localhost:3000/admin](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](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.

```typescript title="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

```typescript title="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:

**Code-First**

```typescript title="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.

**Visual Schema Builder**

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:

```typescript title="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

* [Project Structure](https://nextlyhq.com/docs/getting-started/project-structure.md) -- Understand how the project is organised
* [Fields](https://nextlyhq.com/docs/configuration/fields.md) -- Explore all available field types
* [Direct API](https://nextlyhq.com/docs/api-reference/direct-api.md) -- Full API reference for querying content
* [Media Storage](https://nextlyhq.com/docs/guides/media-storage.md) -- Set up image and file uploads
