Pivot CMS
Features Architecture Pricing Docs FAQ Buy Pro

Documentation

Developer reference for Pivot CMS v1

Everything you build in Pivot CMS — components, variants, modules, templates, menu blocks — gets the same API at render time: a small, typed cms object that reads from your content and configuration, plus a Props bag carrying the field values you defined for that component.

This page documents that API. Sections are deep-linkable, so send the URL with the #section hash when referencing one piece in your team's notes.

Jump to: Quick start Component props The cms object Modules Type reference Editor notes

1. Quick start

A complete module + component pair, showing the contract end-to-end.

A module that returns a typed shape

Modules are server-side functions you author in the Developer → Modules panel. The return type annotation on your function is the contract consuming components see in their IntelliSense:

// modules/related_posts/module.ts
export default async function module(cms: CMS): Promise<{
  posts: Post[];
  postTypeName: string | null;
  total: number;
}> {
  const postType = cms.context.postType;
  if (!postType) return { posts: [], postTypeName: null, total: 0 };

  const typeInfo = await cms.postTypes.get(postType);
  const allPosts = await cms.posts.list(postType, {
    perPage: 4,
    orderBy: "date",
    order: "desc",
  });

  const currentSlug = cms.context.post?.slug ?? null;
  const posts = allPosts.filter(p => p.slug !== currentSlug).slice(0, 3);

  return {
    posts,
    postTypeName: typeInfo?.title ?? postType,
    total: posts.length,
  };
}

A component that consumes it

Attach the module to a component (MODULES panel in the component editor), and the data appears under modules.<slug> with the exact shape you declared:

// components/related-posts-widget/variants/default/Component.tsx
export default function Component({ fields, modules, cms }: Props) {
  const { posts, postTypeName, total } = modules.related_posts;

  if (total === 0) return null;

  return (
    <aside className="related">
      <h3>More from {postTypeName}</h3>
      <ul>
        {posts.map(post => (
          <li key={post.slug}>
            <a href={`/${post.slug}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </aside>
  );
}

Inside the editor, typing post. shows autocomplete for slug, title, status, etc. — not because the runtime enforces a schema, but because the type the module declared is the contract the consuming editor reads.

2. Component props

Every component variant exports a default function that receives a single Props object. The Props shape is auto-generated by the editor from the component's field schema and attached modules and shown as a read-only preamble above your editable code.

// Auto-generated from field schema — do not edit directly
interface Props {
  fields: {
    hero_title: string;
    hero_image: string;
    // ...one entry per field you defined
  };
  modules: {
    related_posts: { posts: Post[]; postTypeName: string | null; total: number };
    // ...one entry per attached module
  };
  menuLocations: {
    header_navigation: Menu | null;  // Header Navigation
    // ...one entry per declared menu location
  };
  cms: CMS;
}

export default function Component({ fields, modules, menuLocations, cms }: Props) {
  // your code here
}

2.1 fields

One typed entry per field defined on the component. The value type is inferred from the field's input type (text → string, group → a nested object, blocks → a block structure, etc.). Group fields nest recursively.

2.2 modules

One entry per attached module, typed using the module's declared Promise<...> return type. If the module didn't declare one (or declared Record<string, unknown>), the entry falls back to Record<string, unknown> in the Props shape and devs lose deep IntelliSense for that module. See Modules for the contract.

2.3 menuLocations

Components that declare menu locations (under the MENU LOCATIONS panel in the component editor) receive one entry per location, typed as Menu | null. null means no menu has been assigned to that location yet.

2.4 cms

The CMS content API — same object that modules receive. Covered in detail below.

3. The cms object

A read-only window into your CMS content. All query methods are async so the underlying storage layer can evolve (currently flat-file JSON) without breaking callers.

Sections: context posts postTypes settings files

3.1 cms.context

Information about the page currently being rendered. Read this to make modules and components context-aware (for example, "show related posts of the same type as the current post").

interface CMSContext {
  /** The post currently being rendered, or null on non-post pages */
  post: Post | null;
  /** The post-type slug of the current post, or null */
  postType: string | null;
  /** The current URL / route being rendered */
  url: string;
}

Example — show a banner only on blog posts:

if (cms.context.postType === "blog") {
  return <Banner />;
}

3.2 cms.posts

Read content documents (pages, posts, and custom post types). All results are typed as Post — see Type reference for the full shape.

cms.posts.get(slug: string): Promise<Post | null>
cms.posts.list(
  postType: string,
  options?: PostListOptions,
): Promise<Post[]>

postType is the slug of any post type — "pages", "posts", or any custom type you've defined under Developer → Post Types.

// Latest 5 posts of type "blog"
const latest = await cms.posts.list("blog", { perPage: 5, orderBy: "date", order: "desc" });

// Search posts by title or slug
const matches = await cms.posts.list("blog", { search: "release notes" });

// A single page by slug
const about = await cms.posts.get("about");

3.3 cms.postTypes

Read post-type definitions — useful for surfacing the display name of a type, or iterating across all configured types.

cms.postTypes.get(slug: string): Promise<PostType | null>
cms.postTypes.list(): Promise<PostType[]>

3.4 cms.settings

Read the global site-settings object. Shape varies based on what your Site Shell component exposes.

cms.settings.get(): Promise<SiteSettings>

3.5 cms.files

Read media-library files. Underlying storage is still being wired up — treat results as informational until the file layer is finalized.

cms.files.get(id: string): Promise<Record<string, unknown> | null>
cms.files.list(options?: FileListOptions): Promise<Record<string, unknown>[]>

4. Modules

A module is a developer-authored async function that receives the cms object and returns structured data. The CMS runs each module attached to a component before rendering, then passes the return value to the component as modules.<slug>.

4.1 The function signature

Every module is a default export of an async function named module:

export default async function module(cms: CMS): Promise<ReturnType> {
  // your logic
  return { /* matching ReturnType */ };
}

The CMS executor strips the function wrapper at runtime and evaluates the body. Inline TypeScript syntax inside the body is not transpiled — if you write arr.filter((x: SomeType) => ...), the colon will throw at runtime. Write plain JavaScript in the body; only the outer signature carries types.

4.2 The return-type contract

The return type you declare IS the contract consuming components see. The CMS lifts whatever you put inside Promise<...> at save time and surfaces it as the typed shape in the component editor's auto-generated Props interface.

If you declare a specific shape, components attaching this module get full IntelliSense:

export default async function module(cms: CMS): Promise<{
  posts: Post[];
  total: number;
}> { /* ... */ }

If you leave it as Record<string, unknown>, consumers see the same generic fallback they'd see if you hadn't declared a type. They can still access fields, but autocomplete stops at modules.<slug>. with no deeper inference.

You can reference any of the public types (below) inside your return type — Post[], Menu, PostType, etc. — and Monaco resolves them inside the editor. You can also declare your own auxiliary types in the module body; they don't get lifted, but they help your own code stay readable.

4.3 Failure modes

  • If the module throws at runtime, the executor logs the error and the component receives no entry for that module slug. Guard with modules.<slug>?. in components.
  • If the module returns a non-object (string, array, number), the executor coerces to an empty object and warns.
  • If you change the declared return type after components are using the module, the consuming components silently keep the new shape on next save — their preamble updates automatically.

5. Type reference

Every type below is available inside both the module editor and the component editor — reference them freely in your function signatures, return types, and component code.

Type Purpose Key fields
Post A content document — page, post, or custom post type. title, slug, contentType, status, description, tags, thumbnail, meta, blocks, publishedAt, ...
Page Alias for Post — pages and posts share the same on-disk shape. Same as Post.
PostType A post-type definition. title, slug, builtin?, template?
Menu A resolved menu assigned to a menu location. title, slug, locations, items
MenuItemNode A single item in a menu tree (nested via children). label, url, target, cssClasses, sourceType, children
ContentMeta SEO/Open Graph metadata for a Post. title, description, ogImage
ContentBlockConfig A single component instance inside a Post's blocks. component, label, variant, fieldValues
PostListOptions Filter/pagination options for cms.posts.list(). page, perPage, order, orderBy, status, search
FileListOptions Filter/pagination options for cms.files.list(). page, perPage, search
CMSContext The cms.context object. post, postType, url
SiteSettings The global site-settings object. Shape depends on your Site Shell.

6. Editor notes

6.1 IntelliSense in the Monaco editor

The component editor and module editor both inject the type declarations above via Monaco's addExtraLib. You don't need to import anything — Post, CMS, etc. are in scope automatically.

6.2 Semantic validation

The component editor disables Monaco's semantic validation because the editable code is a function body wrapped in an auto-generated preamble — semantic checks would surface false positives (missing imports, etc.) for code that's actually fine at runtime. Syntactic errors are still highlighted.

The module editor leaves semantic validation on, so type errors inside module code are flagged in red.

6.3 Opening module files directly in your IDE

Modules are stored on disk at pivot-cms-data/definitions/modules/<slug>/module.ts. If you open one in VSCode or another TypeScript-aware editor, you may see "Cannot find name CMS" or similar diagnostics. This is expected — the pivot-cms-data/ directory is excluded from the CMS's TypeScript project (the types are injected at editor time inside the CMS, not from disk). The runtime ignores type annotations entirely.

The intended editing surface is the in-CMS Monaco editor at Developer → Modules → <module>, which has the right type context loaded.

6.4 Hot reload after editing

Saving a module clears the page cache, so the next request to a page using the module gets a fresh render. No restart needed.

7. Help

Found a gap in the API, broken type declaration, or want a new helper? Send a note via the contact form at labworks.pivot.gdn. Include the version of Pivot CMS you're running — you can find it in the user dropdown at the top-right of the admin.

© 2026 Pivot Labworks LLC. All rights reserved.

Pivot CMS home · Terms / EULA · Privacy