Documentation
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.
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.
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.