Editorial content

Sanity CMS

Schemas, Studio, GROQ, Portable Text, and the Content API — how ReactBD lets editors publish without a deploy.

SchemasGROQStudioContent API

Why a CMS next to MongoDB

MongoDB is excellent for application data (users, tools, carts). It is a poor CMS: no real editing UI, no draft/publish, no portable text, no image pipeline. Sanity is a content lake with Studio. Editors publish; Next.js fetches.

Store in SanityStore in MongoDB
Posts, courses, landing copyUsers, sessions, tools catalog
Portable Text, figures, SEO fieldsOrders, shops, licenses
Draft → publish workflowHigh-frequency writes

Documents

Everything is a document with _id, _type, and fields you define. A post is not a page in the CMS — the website decides the URL (/blog/[slug]) from slug.current.

Schemas

A schema is TypeScript: name, type: "document", and fields. Keep them under sanity/ (or apps/web/sanity) and generate types so queries stay honest.

post.ts
import { defineField, defineType } from "sanity";

export const post = defineType({
  name: "post",
  title: "Post",
  type: "document",
  fields: [
    defineField({ name: "title", type: "string", validation: (r) => r.required() }),
    defineField({ name: "slug", type: "slug", options: { source: "title" } }),
    defineField({ name: "excerpt", type: "text" }),
    defineField({ name: "body", type: "array", of: [{ type: "block" }] }),
    defineField({ name: "publishedAt", type: "datetime" }),
  ],
});

Studio

Studio is the editing UI (/studio or a hosted project). Desk structure, previews, and plugins live next to the schema. Editors never open the Git repo. Developers never copy-paste blog HTML.

  • Drafts are separate from published documents.
  • Role-based access belongs in the Sanity project, not in Next.js pages.
  • Custom input components only when a field is painful — default string/text/image cover 90%.

GROQ

GROQ is Sanity’s query language. Filter by _type, project fields, follow references. Wrap queries in defineQuery so typegen can see them.

posts.query.ts
*[_type == "post" && defined(slug.current)]
  | order(publishedAt desc) {
    title,
    "slug": slug.current,
    excerpt,
    publishedAt
  }

Portable Text

body is an array of blocks, not a markdown string. Render it with @portabletext/react (or a small custom serializer) so headings, links, and figures map to your design tokens — not to Sanity’s default HTML.

Images

Sanity stores the asset; you build URLs with @sanity/image-url. Cap Open Graph images to 1200×630. This repo has images.unoptimized: true — size on the Sanity URL, not via next/image optimization.

og.ts
urlFor(post.mainImage).width(1200).height(630).url()

Fetching in Next.js

Query on the server. ReactBD wraps the client in serverFetch with revalidate in production and revalidate: 0 in dev so Studio edits show up immediately locally.

server-queries.ts
export async function getAllPosts() {
  return serverFetch(allPostsQuery, {}, {
    revalidate: 1800,
    tags: ["posts"],
  });
}

Revalidation

When an editor publishes, a webhook (or a Studio action) should revalidateTag("posts"). Until then, the revalidate TTL is the worst-case delay. Do not force-dynamic a blog index just to see a new post — tag it.

How ReactBD uses Sanity

  • Courses, some marketing pages, and CMS-backed listings query Sanity on the server.
  • Sitemap merges static routes with Sanity slugs (getAllCourses, getMaterialsSlug).
  • The study materials you are reading are not Sanity — they are TypeScript modules, generated statically on purpose.
  • When you add a document type, add its public URLs to app/sitemap.ts.

Related

Part of the ReactBD MERN documentation. Static content, rendered on the server.