SleekCMS All In One Documentation for AI Agents SleekCMS is a headless content management system with an integrated static site builder. You define structured content models, create and manage content through a visual editor, and either consume that content via API or publish it as a fully built static site — no Git repositories, no build pipelines, no servers to manage. This page introduces the architecture, core concepts, and use cases that shape how you work with SleekCMS. --- ## What is SleekCMS? SleekCMS combines two capabilities that are typically separate: **A headless CMS** — You model structured content (pages, blog posts, authors, settings) and access it through delivery APIs (REST and GraphQL). Your frontend applications fetch content at build time or runtime, just like any other headless CMS. **A static site builder** — You design view templates in an integrated coding environment, and SleekCMS compiles your content and templates into a static site deployed to a global CDN. There is no external build step. You don't push code to a repository or configure a CI/CD pipeline; the platform handles generation and deployment end to end. You can use either capability independently or both together. Build a marketing site entirely within SleekCMS, power a React or Next.js app through the content API, or do both from the same content models. ### How it differs from traditional CMSs Traditional monolithic CMSs (WordPress, Drupal) couple content storage with presentation. Your content lives inside themes and page templates, making it difficult to reuse across channels. Headless CMSs solve the coupling problem but typically require you to set up your own frontend, build tooling, and hosting. SleekCMS gives you that headless flexibility while also providing an optional, zero-config site builder for teams that don't want to maintain a separate frontend stack. | Capability | Traditional CMS | Headless CMS | SleekCMS | |---|---|---|---| | Structured content modeling | Limited | Yes | Yes | | Content API (REST/GraphQL) | Requires plugins | Yes | Yes | | Built-in site rendering | Yes (coupled) | No | Yes (decoupled) | | No-code publishing | Yes | No | Yes | | Multi-channel delivery | Difficult | Yes | Yes | ### Who it is for - **Developers** who want structured content APIs with TypeScript-friendly schemas, and optionally a built-in rendering layer they don't have to maintain. - **Content teams** who need a clean editing experience with visual page building, draft workflows, and no dependency on engineering for publishing. - **Agencies** managing multiple client sites from a single organization, with reusable content models and isolated environments. --- ## Core Use Cases **Marketing websites** — Build landing pages, product pages, and campaign microsites using block-based layouts. Content editors assemble pages visually while developers define the available building blocks. **Blogs and editorial publishing** — Model authors, categories, and posts as structured content. Use page collections with slug-based routing for automatic URL generation. Publish through draft-to-published workflows. **Landing page builders** — Define a library of section blocks (hero, features, testimonials, pricing) and let editors compose pages without developer involvement. Dynamic blocks give editors layout flexibility within guardrails you control. **Multi-brand and multi-site setups** — Manage multiple sites under one organization. Share content models across sites or keep them isolated. Each site gets its own content, templates, and deployment. **Design systems and reusable components** — Model UI components as blocks with typed fields. Reference the same block across multiple page models. Changes to a block's structure propagate everywhere it's used. **Structured content APIs for apps** — Use SleekCMS purely as a headless backend. Fetch content through REST or GraphQL delivery APIs for mobile apps, SPAs, digital signage, or any frontend that consumes JSON. --- ## Key Concepts SleekCMS organizes content around four primitives. Understanding these is essential before you start modeling. ### Models A model is a schema definition. It declares the fields, structure, and behavior of a content type. You create models in the admin UI, and each model produces a corresponding content editing interface and API shape. There are three kinds of models: page models, entry models, and block models. ### Pages A page represents a routable piece of content — something with a URL. Page models define the fields and layout structure for pages. Pages come in two forms: - **Static pages** — A single instance tied to a fixed path (e.g., `/about`, `/contact`). You define the path in the model. - **Page collections** — Multiple instances generated from a slug field (e.g., `/blog/my-first-post`). SleekCMS generates routes automatically based on the slug. Pages can contain dynamic block fields, which let editors compose the page layout from a set of allowed block types. ### Entries An entry is a piece of structured content that is **not directly routable**. Entries don't have URLs on their own — they exist to be referenced by pages or other entries. Common examples include authors, categories, site-wide settings, and any reusable data that multiple pages might share. You reference entries through relational fields (one-to-one or one-to-many). Entries can also be configured as admin-only, making them useful for global settings like navigation menus, footer content, or feature flags that editors shouldn't duplicate across pages. ### Blocks A block is a **reusable, composable content component**. Blocks define a structured set of fields that represent a visual section or UI element — a hero banner, a call-to-action card, a testimonial carousel. Blocks differ from entries in two important ways: - Blocks are embedded inline within pages or other content. They don't exist independently in a content list. - Blocks are designed for visual composition. Editors add, remove, reorder, and hide blocks within a page's dynamic block field. Blocks can be nested inside other blocks, enabling complex but governed layouts. ### How they relate The content architecture follows a clear hierarchy: ``` Page Model ├── Fields (title, metadata, SEO) ├── Reference Fields → Entry Models (author, category) └── Dynamic Block Field ├── Hero Block │ └── Fields (heading, image, CTA) ├── Features Block │ └── Fields (items collection, layout) └── Testimonials Block └── Fields (quotes, author references → Entry) ``` A **page** is the top-level routable container. It references **entries** for shared data and contains **blocks** for composable layout sections. **Models** define the schema for all three. --- ## What's Next - [Getting Started](/getting-started) — Create your first project, set up models, and publish a page. - [Content Modeling](/model) — Deep dive into pages, entries, blocks, fields, and how to design your content architecture. - [Site Builder](/builder) — Learn the integrated coding environment, view templates, and deployment. - [Content API](/publish/api) — REST and GraphQL endpoints for headless consumption. This section walks through the complete lifecycle of building and publishing your first project in SleekCMS. By the end, you will: - Create an organization - Create a site (from template or blank) - Define basic models - Create content - Publish your first page - Understand the developer/editor workflow ------------------------------------------------------------------------ # 1. Create an Organization An **Organization** (or Account) is the top-level container in SleekCMS. An organization can contain: - One or more Sites - Multiple users - Role-based permissions - Shared billing and access control Think of an organization as your workspace boundary. ------------------------------------------------------------------------ # 2. Create a Site A **Site** is where modeling, content, and rendering live. Each organization can contain multiple sites. ## Choose How to Start ### Option A --- Start from a Site Template SleekCMS supports complete, clonable site templates. A template includes: - Page models - Entry models - Block models - Option sets - View templates - Sample content - Site configuration Templates are shared using a **clone token**. > Every site in SleekCMS can be cloned. Every site is also a template. ![](https://img.sleekcms.com/2zih/mlis40b8.webp?w=600&h=400&b=1&bc=aaa&round=8&p=8) *Template selection screen showing multiple ready-to-use site templates* ------------------------------------------------------------------------ ### Option B --- Start with a Blank Site A blank site contains: - No models - No content - No rendering configuration ------------------------------------------------------------------------ # 3. Set Up Basic Models (If Starting Blank) Create: - One Page Model (Homepage) - One Block Model (Hero Section) - Optional Entry Model (Author) ## Create a Block Model (Hero Section) Example fields: - Heading (Text) - Subheading (Text) - Background Image (Image) - CTA Label (Text) - CTA URL (Link) ------------------------------------------------------------------------ ## Create a Page Model (Homepage) (Path / ) Fields: - Title (Text) - SEO Description (Text) - Sections (Dynamic Block field allowing Hero, CTA, etc.) ------------------------------------------------------------------------ ## Optional: Create an Entry Model (Author) Example fields: - Name (Text) - Profile Image (Media) - Bio (Rich Text) ------------------------------------------------------------------------ # 4. Create Your First Page 1. Create a new page using your Homepage model. 2. Fill basic fields. 3. Add blocks to the Dynamic Block field. 4. Configure block content. ------------------------------------------------------------------------ # 5. Configure Site Builder (Required for Preview) To preview or publish rendered pages, configure the Site Builder. Steps: 1. Enable Site Builder. 2. Attach a View Template to each Page Model. 3. Attach View Templates to Block Models. 4. Provide EJS templates. Example minimal EJS templates: ```erb <% title(item.title) %>

<%= item.title %>

<%- render(page.sections) %> ``` ```erb

<%= item.heading %>

``` Content can exist without rendering. Rendering requires View Templates. ------------------------------------------------------------------------ # 5. Publish 1. Preview. 2. Add Deploy Targets (Netlify, Vercel, Surge, and more) 4. Publish. ------------------------------------------------------------------------ # Basic Workflow Model → Create Content → Render ------------------------------------------------------------------------ ## Developer Responsibilities - Create and maintain models - Design content architecture - Configure site templates - Implement rendering templates - Maintain structural integrity ------------------------------------------------------------------------ ## Content Editor Responsibilities - Create and update content - Assemble pages using blocks - Manage entries - Publish content ![](https://img.sleekcms.com/2zih/mlisilro.webp?w=600&h=240&b=1&round=8&p=8&bc=aaa) ------------------------------------------------------------------------ # Summary You have: - Created an Organization - Created a Site (template or blank) - Defined models (if starting blank) - Created structured content - Published a page SleekCMS enables: 1. Intentional content modeling 2. Reusable site architectures 3. Safe delegation to content teams 4. Flexible rendering across environments Content modeling is the practice of defining the structure, types, and relationships of the content your site or application will use. In SleekCMS, every piece of content — a page, a blog post, an author profile, a hero banner — is backed by a model that declares its shape as a set of typed fields. This page explains the modeling philosophy behind SleekCMS, introduces the three model types and how they relate, and covers the structural field concepts you'll use to organize content within models. --- ## Why Structured Content Matters Unstructured content is a single blob of rich text — a WordPress post body, a Google Doc, a Notion page. It mixes data with formatting and gives you no reliable way to extract individual pieces (the author name, the CTA button label, the hero image) for use in different contexts. Structured content separates every meaningful piece of data into its own typed field. A blog post isn't a rich text blob; it's a model with discrete fields for title, slug, author reference, published date, excerpt, featured image, and a body composed of ordered content blocks. Each field has a type (text, image, reference, boolean), validation rules, and a clear contract with the templates and APIs that consume it. This matters for three reasons: **Multi-channel delivery.** The same content model can feed a static site, a mobile app, a third-party integration, and a future redesign — because the data isn't entangled with any single presentation. **Editor experience.** Content editors work with purpose-built forms rather than free-form text. They pick an author from a dropdown, toggle a featured flag, reorder section blocks. The editing interface is derived directly from your model, so the structure you design determines the experience they get. **Governance at scale.** When every page and component has a defined schema, you control what content can exist and where. An editor can't accidentally break a layout by deleting a required field or inserting an unsupported element. --- ## The Three Model Types SleekCMS has three model types. Each serves a different architectural role, and understanding when to use which one is the most important modeling decision you'll make. ### Page Models A page model defines routable content — content that maps to a URL and renders as a page on your site. Pages are the top-level containers in your content architecture. They hold fields for metadata (title, description, OG image), SEO configuration, and most importantly, the layout structure that determines what appears on the page. Pages can include dynamic block fields that let editors compose the visual layout from a set of allowed blocks. Pages come in two variants: - **Static pages** are single instances bound to a fixed path you define in the model (e.g., `/about`, `/pricing`). One model, one page, one URL. - **Page collections** generate multiple page instances from a slug field (e.g., `/blog/:slug`). SleekCMS creates routes automatically for each instance. Use page models whenever content needs its own URL. → [Page Models in depth](/model/pages) ### Entry Models An entry model defines structured data that is **not directly routable**. Entries don't have URLs — they exist to be referenced by pages, other entries, or consumed via the content API. Entries are your shared, reusable data objects. An author entry holds a name, bio, avatar, and social links. A category entry holds a label and slug. A site settings entry holds global navigation items, footer text, and feature flags. Pages and blocks reference these entries through relational fields, and a single entry can be referenced from many places. Entries can be configured as admin-only, which makes them invisible to content editors and ideal for global configuration that shouldn't be duplicated or accidentally modified. Use entry models for shared data, lookup tables, configuration objects, and anything that multiple pages or blocks need to reference. → [Entry Models in depth](/model/entries) ### Block Models A block model defines a **reusable, composable content component** — a self-contained section or UI element that can be placed inside pages (or other blocks). Blocks are the building blocks of visual layout. A hero block might have fields for heading, subheading, background image, and CTA button. A testimonials block might have a collection of quote items with author references. A pricing table block might have tier entries and a toggle for annual vs. monthly billing. Blocks differ from entries in how they're used: - Blocks are **embedded inline**. They don't appear in a standalone content list; they exist inside the pages and content that contain them. - Blocks are **composable**. Editors add, remove, reorder, and hide blocks within a page's dynamic block field, assembling layouts from the components you've defined. - Blocks can be **nested**. A block can contain other blocks, enabling complex but governed layout hierarchies. The same block model can be used across multiple page models. If you update the block's field structure, the change applies everywhere that block is used. Use block models for visual sections, UI components, and any repeatable layout element that editors should be able to compose into pages. → [Block Models in depth](/model/blocks) ### Choosing the Right Model Type | Question | Answer | Use | |---|---|---| | Does it need its own URL? | Yes | Page model | | Is it shared data referenced from multiple places? | Yes | Entry model | | Is it a visual section editors compose into pages? | Yes | Block model | | Is it global config (nav, footer, feature flags)? | Yes | Entry model (admin-only) | | Is it a standalone data record accessed via API? | Yes | Entry model | --- ## Fields as Data Structures Every model is made up of fields. Fields are the atomic units of your content schema — each one defines a named, typed piece of data with optional validation and display rules. SleekCMS groups fields into four categories: **Textual fields** handle strings, rich text, numbers, booleans, dates, and other scalar values. These are the most common fields and cover the majority of your content data. **Media fields** handle images, files, and other uploaded assets. Image fields integrate with image processing (via Imgix) for automatic resizing, format conversion, and optimization. **Structural fields** organize other fields within a model. These don't hold content data themselves — they define how fields are grouped, repeated, or composed. There are three structural field types, and understanding the distinction between them is critical. **Relational fields** create references between models. A reference field on a blog post page model might point to an author entry model, letting editors pick an author from a dropdown. References can be one-to-one or one-to-many. → [Field Types reference](/model/fields) --- ## Structural Fields: Group, Collection, and Block Three field types control how content is structured within a model. They look similar at first glance but serve fundamentally different purposes. ### Group A group is a **container for organizing fields visually**. It doesn't add a new data structure — it simply nests related fields together in the editor UI and optionally arranges them into columns. Groups are a modeling convenience. Use them to cluster related fields (e.g., grouping "Street," "City," "State," and "Zip" under an "Address" group) so the editor form is scannable and logical. You can reorganize groups without affecting your data. Groups are **not reusable** across models. They exist only within the model where you define them. → [Group fields](/model/field/group) ### Collection A collection is a **repeatable inline structure**. It defines a set of fields that editors can duplicate as multiple items within a single content instance — an FAQ list, a feature grid, a set of bullet points. Each item in a collection has the same field structure, and editors can add, remove, and reorder items. Collections are defined and edited inline; the data lives within the parent model instance. Collections are **not reusable** across models. Like groups, they're defined within a single model. If you need the same repeatable structure in multiple models, use a block instead. → [Collection fields](/model/field/collection) ### Block (as a field) A block field **embeds a block model** inside another model. Unlike groups and collections, the structure is defined externally in a block model and referenced by the field. This means: - The same block can be used across multiple models. - Changes to the block model propagate to every instance. - Each block has its own view template, separating content structure from presentation. Block fields can reference a single block type or allow multiple types (via dynamic blocks). → [Block fields](/model/fields/block) ### Comparison | Capability | Group | Collection | Block | |---|---|---|---| | Organizes related fields | Yes | Yes | Yes | | Repeatable (multiple items) | No | Yes | Yes (via dynamic blocks) | | Reusable across models | No | No | Yes | | Has its own view template | No | No | Yes | | Defined externally | No | No | Yes | | Nestable | Yes | No | Yes | The rule of thumb: use **groups** for visual organization, **collections** for simple repeatable lists within a model, and **blocks** for reusable components shared across models or that need independent templates. --- ## Content Architecture The full content architecture in SleekCMS follows a layered hierarchy: ``` Page Model (routable container) │ ├── Scalar Fields │ ├── Title (text) │ ├── Meta Description (text) │ └── Published Date (date) │ ├── Group Field │ └── SEO Settings │ ├── OG Image (image) │ └── Canonical URL (text) │ ├── Reference Fields → Entry Models │ ├── Author → Author Entry │ └── Categories → Category Entry (one-to-many) │ └── Dynamic Block Field (composable layout) ├── Hero Block │ ├── Heading (text) │ ├── Background (image) │ └── CTA Group │ ├── Label (text) │ └── URL (text) │ ├── Content Block │ └── Body (rich text) │ └── Testimonials Block └── Quotes (collection) ├── Quote Text (text) └── Author → Author Entry (reference) ``` Content flows from models (schema) to instances (data) to templates (presentation). The model defines what fields exist. The editor creates instances by filling in those fields. The site builder or API delivers the content in the shape the model defines. --- ## Separation of Content and Layout SleekCMS enforces a deliberate separation between content structure and visual presentation: **Models** define data shape. A hero block model declares that a hero has a heading, a subheading, an image, and a CTA — but says nothing about how those fields render. **View templates** define presentation. A hero block's view template determines the HTML, styling, and layout. The same block model could have different templates for different contexts or sites. This separation means you can redesign a site without restructuring content, reuse content across multiple presentation layers, and give editors a clean, form-based experience that doesn't expose HTML or styling concerns. --- ## Impact on the Editor Experience Every modeling decision you make directly shapes what content editors see and do. This is worth considering intentionally: - The **fields you add** determine the form inputs editors interact with. A reference field becomes a dropdown. A boolean becomes a toggle. A collection becomes a repeatable item list. - The **groups you create** determine how the form is organized. Logical grouping reduces cognitive load and makes large models manageable. - The **blocks you allow** in a dynamic block field determine what layout options editors have. This is your primary lever for balancing flexibility (editors can compose freely) with governance (editors can only use approved components). - The **required and validation rules** you set prevent incomplete or malformed content from being published. Well-modeled content makes editors faster and more confident. Poorly modeled content leads to workarounds, inconsistency, and support requests. --- ## What's Next - [Page Models](/model/pages) — Routing, static pages, page collections, and dynamic block layouts. - [Entry Models](/model/entries) — Reusable data, references, and admin-only configuration. - [Block Models](/model/blocks) — Composable components, nesting, and cross-model reuse. - [Option Sets](/model/options) — Centralized key-value taxonomies for dropdown fields. - [Field Types](/model/fields) — Complete reference for every field type and its capabilities. A page model defines the schema for routable content — any piece of content that has a URL. When you create a page model, you're declaring the fields, path structure, and layout capabilities for a type of page on your site. SleekCMS uses that model to generate the editing interface for content creators and the API shape for developers. This page covers how page models work, the two routing modes, dynamic blocks for composable layouts, and how to think about structuring page content. --- ## What Is a Page Model A page model is a content type definition where each instance produces a page with its own URL. This is what distinguishes pages from entries — entries are structured data without routes, while pages are the routable layer of your site. When you create a page model, you configure three things: **A path** — The URL structure for pages of this type. This determines where the page lives on your site and how its route is generated. **Fields** — The data structure for the page's content. These can include text fields, images, reference fields pointing to entries, and structural fields like groups and collections. **Dynamic block fields** — Optional fields that let content editors compose the page layout from a set of allowed block types. This is how you give editors layout flexibility while maintaining structural control. The result is a reusable schema. A "Blog Post" page model might define a title, author reference, publish date, featured image, and a dynamic block field for body sections. Every blog post created from that model shares the same structure, the same editing interface, and the same API shape. --- ## Path and Routing Every page model has a path that determines the URL structure for its pages. SleekCMS supports two routing modes, and the one you choose depends on whether the model produces a single page or many pages. ### Static Pages A static page model produces exactly one page at a fixed path. You define the path in the model, and there is only ever one content instance for that model. Common examples include a homepage (`/`), an about page (`/about`), a contact page (`/contact`), or a pricing page (`/pricing`). These are pages where you don't need multiple instances — there's one about page, not a collection of them. When you set a page model's path to `/about`, SleekCMS creates one editable page at that path. In the site builder, this generates `about/index.html`. Through the content API, you fetch it with `getPage('/about')`. ### Page Collections A page collection model produces multiple pages under a shared path prefix, with each page differentiated by a slug. This is the model type for blogs, product catalogs, case studies, documentation — any content type where you need many pages with the same structure but different content. When you create a page collection, you define a base path and add a slug field to the model. SleekCMS generates routes automatically by combining the base path with each page's slug value. For example, a blog page model with the path `/blog` and posts slugged `hello-world` and `getting-started` produces two routes: `/blog/hello-world` and `/blog/getting-started`. Each new blog post a content editor creates automatically gets its own route based on its slug. ### The Slug Field The slug field is a special text field that controls the URL segment for each page in a collection. When an editor creates a new page from a collection model, they provide a slug value — typically a URL-friendly version of the title. Slugs must be unique within a collection. SleekCMS uses them for route generation, and duplicate slugs would produce conflicting URLs. Slugs are also what you pass to the content API to retrieve a specific page: `getPage('/blog/hello-world')`. ### Choosing Between Static and Collection The decision is straightforward. If there will only ever be one instance of this page type, use a static page. If editors will create multiple pages with the same structure, use a collection. | Scenario | Model Type | Path Example | |---|---|---| | Homepage | Static | `/` | | About page | Static | `/about` | | Blog posts | Collection | `/blog/:slug` | | Product pages | Collection | `/products/:slug` | | Case studies | Collection | `/work/:slug` | | Contact page | Static | `/contact` | --- ## Structuring Page Fields Page model fields define what content editors fill in when they create or edit a page. The fields you add to a page model determine both the editing experience in the admin UI and the data shape in the API response. ### Common Field Patterns Most page models start with a core set of fields that cover the page's primary content and metadata. **Title and slug** — Nearly every page model includes a title field. Collection models also include the slug field for route generation. **SEO and metadata** — Fields for meta title, meta description, and Open Graph image give content editors control over how pages appear in search results and social media previews. Grouping these into a dedicated field group keeps the editor interface organized. **Featured media** — Hero images, banner videos, or thumbnail images that represent the page visually. These are typically image or media fields. **Reference fields** — Links to entries like authors, categories, or tags. A blog post model might reference an author entry (one-to-one) and multiple category entries (one-to-many). References keep shared data centralized rather than duplicated across pages. ### Organizing with Groups When a page model has many fields, groups help organize them into logical sections in the editor. You might group SEO fields together, put hero section fields in their own group, or separate primary content from sidebar content. Groups affect the editing interface layout but don't change the data structure — they're purely organizational. --- ## Dynamic Blocks in Pages Dynamic block fields are where page models become composable. A dynamic block field is a special field type that lets editors build a page's layout by adding, removing, reordering, and hiding block instances from a set of allowed block types. ### How It Works When you add a dynamic block field to a page model, you configure which block types editors are allowed to use. An editor creating a page then assembles the layout by choosing from those blocks — adding a hero section, then a features grid, then a testimonial carousel, then a call-to-action banner. Each block instance in the dynamic field is an independent content section with its own fields, defined by the block model. The page becomes a stack of sections, where each section's structure is governed by its block model but the overall composition is up to the editor. ### Layout Governance Dynamic blocks give editors significant flexibility, but you control the boundaries. The allowed block types list is your governance mechanism — editors can only add blocks you've explicitly permitted on that page model. This means you can create tightly controlled page models where editors choose from just two or three section types, or more open-ended models with a large library of blocks. The balance between flexibility and consistency is yours to set per page model. ### When to Use Dynamic Blocks Dynamic blocks are ideal when editors need to compose pages with varying layouts — landing pages, marketing pages, long-form content with mixed media sections. They're less necessary for pages with a fixed, predictable structure. A simple "About" page with a fixed set of fields doesn't need dynamic blocks. A landing page that might have anywhere from three to twelve sections in different orders does. Not every page model needs a dynamic block field. Use them when the layout varies across instances; use fixed fields when every page of that type has the same structure. --- ## Page Models and the Site Builder If you're using the integrated site builder, each page model is bound to an EJS template. The template defines how the page's content renders to HTML. When the builder compiles your site, it processes each page record through its model's template and produces a static HTML file. For static pages, the builder generates one HTML file at the model's path. For page collections, it generates one file per record using the slug to differentiate output paths. Dynamic block fields render by delegating to each block's own template. The page template includes the dynamic block field, and the builder iterates through the block instances, rendering each one with the appropriate block template and injecting the output into the page. This is how composable layouts work at the template level — the page template orchestrates, and block templates handle the individual sections. → [Site Builder](/builder) → [Model Templates](/builder/code/main) --- ## Page Models and the Content API If you're consuming content through the API rather than the site builder, page models define the JSON shape of your API responses. Fetching a static page returns its fields as a JSON object. Fetching pages from a collection returns an array of objects, each with the same field structure. The `getPage()` method retrieves a single page by exact path, while `getPages()` retrieves all pages under a path prefix. The `getSlugs()` method returns the slug values for a collection, which is useful for generating static routes in frameworks like Next.js or Astro. ```typescript // Static page const about = client.getPage('/about'); // Single collection page const post = client.getPage('/blog/hello-world'); // All collection pages const posts = client.getPages('/blog'); // Slugs for static generation const slugs = client.getSlugs('/blog'); // ['hello-world', 'getting-started', 'advanced-techniques'] ``` Dynamic block fields appear in the API response as an array of block objects, each with a type identifier and its field data. Your frontend code can iterate over these blocks and render the appropriate component for each type. → [Content API](/publish/api) → [@sleekcms/client](/publish/api/client) --- ## What's Next - [Entry Models](/model/entries) — Structured, non-routable content for shared data like authors, categories, and settings. - [Block Models](/model/blocks) — Reusable, composable content components for dynamic page sections. - [Content Field Types](/model/fields) — The full set of field types available in page, entry, and block models. - [Dynamic Blocks](/model/fields/dynamic) — Deep dive into configuring and using dynamic block fields. - [Content Editing](/content) — The editing experience for content creators working with page models. An entry model defines the schema for structured content that is not directly routable. Entries don't have URLs — they exist to hold reusable data that pages and other entries reference. When you need shared content that appears in multiple places across your site, or global data like site settings and navigation, you model it as an entry. This page covers how entry models work, the different ways entries are referenced and reused, admin-only entries for site-wide settings, and how to decide between entries and pages. --- ## What Is an Entry Model An entry model is a content type definition where each instance is a standalone piece of structured data without its own URL. This is what distinguishes entries from pages — pages are routable and produce URLs, while entries are the supporting data layer that pages draw from. When you create an entry model, you configure two things: **A handle** — A unique identifier for the entry model, used to retrieve entries through the content API and to reference them in templates. The handle is how your code and templates address this entry type. **Fields** — The data structure for the entry's content. Entry fields work the same way as page fields — text, images, groups, collections, and references to other entries are all available. The result is a reusable data structure that can be referenced from anywhere. An "Author" entry model might define fields for name, bio, headshot, and social links. A "Site Settings" entry model might define fields for logo, company name, footer text, and social media URLs. These entries are created once and referenced wherever they're needed. --- ## Entries as Reusable Data The core purpose of entries is content reuse. Instead of duplicating the same information across multiple pages, you store it in one place and reference it. Consider a blog with ten authors. Without entries, you'd add author name, bio, and headshot fields directly to the blog post page model. Every time an author's bio changes, you'd need to update it on every post they wrote. With an Author entry model, each author exists as a single entry. Blog posts reference the author entry, and changes to an author's bio propagate everywhere that entry is referenced. This pattern applies broadly. Team members referenced on both an "About" page and individual case study pages. Categories shared across blog posts and product pages. Testimonials that appear on the homepage and a dedicated reviews page. Any time the same content appears in more than one place, entries keep it centralized. --- ## Entry Collections An entry model can produce a single entry or a collection of entries. The distinction depends on the nature of the content. **Single entries** — Some entry models represent one-of-a-kind data. A "Header" entry holds your site's navigation links. A "Footer" entry holds your footer columns and copyright text. A "Site Settings" entry holds your logo, tagline, and global metadata. These models produce exactly one entry because there's only one header, one footer, one set of site settings. **Entry collections** — Other entry models represent a category of content with multiple instances. An "Author" model produces one entry per author. A "Category" model produces one entry per category. A "Team Member" model produces one entry per person. Editors create as many entries as needed, and each entry is available for referencing from pages or other entries. The model itself doesn't require explicit configuration to be a single or collection — this is determined by how many instances you create. However, admin-only entries (covered below) are typically used for single-instance models where you want to restrict creation. --- ## Referencing Entries Entries become useful when other content references them. SleekCMS provides reference fields that create relationships between content, and these relationships come in two forms. ### One-to-One References A one-to-one reference field links a page or entry to a single entry of a specific type. A blog post referencing its author is a one-to-one relationship — each post has one author. When an editor fills in a one-to-one reference field, they see a dropdown of available entries from the referenced model. They select one, and the relationship is established. In the API response, the referenced entry's data is included inline, so your frontend code has immediate access to the full author object without a separate API call. ### One-to-Many References A one-to-many reference field links a page or entry to multiple entries of a specific type. A blog post referencing its categories is a one-to-many relationship — each post can belong to several categories. Editors select multiple entries from the available options, and all selected entries appear in the API response as an array. This is how you model tags, categories, related products, contributor lists, or any relationship where one piece of content connects to many entries. ### Reference Patterns References are the connective tissue of your content architecture. A few common patterns: **Author attribution** — A page collection (blog posts, case studies) references an Author entry model. Each post links to one author. The author's name, bio, and photo are maintained in one place and appear consistently across all their posts. **Taxonomy and categorization** — A page collection references a Category or Tag entry model through a one-to-many field. Editors assign categories when creating content. Your frontend uses these references to build category pages, filter interfaces, or related content suggestions. **Shared components** — An entry model stores content for a reusable element — a promotional banner, a featured testimonial, an announcement bar. Multiple pages reference the same entry, and updating the entry updates it everywhere. **Cross-referencing entries** — Entries can reference other entries, not just pages. A "Team Member" entry might reference a "Department" entry. A "Product" entry might reference a "Brand" entry. This lets you build normalized data structures where relationships are explicit and content is never duplicated. --- ## Admin-Only Entries Entry models can be configured as admin-only, which restricts them to a single entry instance that only administrators can manage. This is the mechanism for global site settings — content that affects the entire site but shouldn't be duplicated or freely created by content editors. Common admin-only entries include: **Navigation** — The site's main navigation links, structured as a collection of menu items with labels, URLs, and optional nested items. **Footer** — Footer columns, copyright text, social media links, and legal page references. **Site settings** — Company name, logo, tagline, default metadata, analytics IDs, and other configuration that applies globally. **Feature flags** — Toggles that control whether certain sections or features appear on the site, managed by administrators without code changes. Admin-only entries appear in a dedicated section of the admin interface rather than in the general content list. This separation keeps the editing experience clean — content editors see the content they manage, while site-wide settings live in their own space. In templates and API responses, admin-only entries are accessed through the `getEntry()` method by handle, just like regular entries. The admin-only distinction affects who can edit them and where they appear in the interface, not how they're consumed. ```typescript // Admin-only entries are accessed the same way as regular entries const header = client.getEntry('header'); const footer = client.getEntry('footer'); const settings = client.getEntry('site-settings'); ``` --- ## When to Use Entry vs Page The distinction between entries and pages is about routability. If the content needs its own URL — a page someone navigates to directly — it's a page. If the content exists to support other content or to store shared data, it's an entry. A few cases that can seem ambiguous: **Team members** — If each team member has their own profile page with a URL (`/team/jane-doe`), model them as a page collection. If team members only appear as cards on an "About" page or as author bylines on blog posts, model them as an entry collection. **Categories** — If each category has a dedicated landing page showing all posts in that category (`/blog/category/design`), you need a page for that. But the category data itself — name, description, icon — is still best modeled as an entry that both the category page and the blog posts reference. You can use both: an entry model for the category data and a page model for the category landing page that references the entry. **Testimonials** — If testimonials are sprinkled across various pages and don't need their own URLs, they're entries. If you want a dedicated `/testimonials` page, that's a page — but the individual testimonials can still be entries that the page references. The general rule: start with entries for any content that will be referenced from multiple places. Promote to pages when the content needs its own route. | Content | Model Type | Reason | |---|---|---| | Blog author | Entry | Referenced by posts, no own URL needed | | Blog post | Page (collection) | Has its own URL | | Navigation links | Entry (admin-only) | Global data, no URL | | Product category | Entry | Shared taxonomy, referenced by products | | Category landing page | Page (collection) | Needs its own URL to list products | | Site footer | Entry (admin-only) | Global layout data | | Team member profiles | Depends | Entry if referenced only; page if individually routable | --- ## Entry Models and the Site Builder If you're using the integrated site builder, entry models can be bound to EJS templates just like page and block models. An entry's template defines how that entry renders when it's included in a page — an author card, a team member bio, a category badge. When a page template encounters a reference to an entry, it can render the entry using the entry's own template. This keeps rendering logic modular — the author card layout is defined once in the author entry template and used consistently everywhere an author is referenced. Entry templates are optional. If an entry model doesn't have a template, you access its field data directly in the page or block template that references it. Templates are most useful when the entry has a visual representation that should be consistent across the site. → [Site Builder](/builder) → [Model Templates](/builder/code/main) --- ## Entry Models and the Content API Through the content API, entries are retrieved by their handle using the `getEntry()` method. Single entries return a JSON object. Entry collections return an array of objects. ```typescript // Single entry (admin-only or single-instance) const header = client.getEntry('header'); // { logo: { url: '...' }, links: [...] } // Entry collection const authors = client.getEntry('authors'); // [{ name: 'Jane', bio: '...', headshot: { url: '...' } }, ...] // Entries referenced from pages are resolved inline const post = client.getPage('/blog/hello-world'); // post.author → { name: 'Jane', bio: '...', headshot: { url: '...' } } // post.categories → [{ name: 'Design', slug: 'design' }, ...] ``` When a page references an entry, the entry's data is resolved inline in the page's API response. You don't need to make a separate call to fetch the referenced entry — it's already embedded in the page data. This applies to both one-to-one and one-to-many references. → [Content API](/publish/api) → [@sleekcms/client](/publish/api/client) --- ## What's Next - [Page Models](/model/pages) — Routable content types with URL paths and slug-based collections. - [Block Models](/model/blocks) — Reusable, composable content components for dynamic page sections. - [Content Field Types](/model/fields) — The full set of field types available in page, entry, and block models. - [Option Sets and Lists](/model/options) — Key-value pairs for dropdowns and centralized taxonomy. - [Content Editing](/content) — The editing experience for content creators working with entry models. A block model defines the schema for a reusable, composable content component — a self-contained section or UI element that editors can add to pages. Blocks are the building blocks of flexible page layouts. A hero banner, a call-to-action card, a features grid, a testimonial carousel — each of these is a block model with its own fields and its own template. This page covers how block models work, how they're used inside pages and entries, nested block architecture, composability across models, and how to decide between blocks and other structural field types. --- ## What Is a Block Model A block model is a content type definition that represents a discrete, reusable section of content. Unlike pages, blocks don't have URLs. Unlike entries, blocks don't exist independently in a content list. Blocks are embedded inline within other content — they live inside the pages and entries that use them. When you create a block model, you configure: **A name and handle** — The name is what editors see when choosing blocks in the editor. The handle is the identifier used in templates and API responses. **Fields** — The data structure for the block's content. A hero block might have fields for heading, subheading, background image, and a CTA button group. A testimonial block might have fields for quote text, author name, author photo, and company. The result is a content component that can be used across your entire site. You define the block model once, and it becomes available to any page or entry model that includes it in its allowed block types. --- ## Blocks Inside Pages The primary use of blocks is within page models through dynamic block fields. When a page model includes a dynamic block field, it specifies which block types editors are allowed to add. Editors then compose the page by selecting and arranging blocks from that allowed set. A homepage page model might allow hero, features, testimonials, pricing, CTA, and FAQ blocks. An editor building the homepage adds the blocks they need, fills in the content for each one, and arranges them in the desired order. The result is a page whose layout is unique to that instance but whose individual sections are all structurally governed by their block models. Each block instance within a page is independent. An editor can add three different CTA blocks to the same page, each with different content. They can reorder blocks, hide blocks without deleting them, and remove blocks entirely. The dynamic block field gives editors full control over composition while the block models maintain structural consistency for each section type. → [Dynamic Blocks](/model/fields/dynamic) --- ## Blocks Inside Entries Blocks aren't limited to pages. Entry models can also include block fields, which lets you build composable structures in non-routable content. A "Service" entry model might include a block field that allows editors to add feature blocks, making each service entry composable. A "Product" entry model might include blocks for specification sections, comparison tables, or highlight panels. The mechanics are the same as in pages — you add a block field to the entry model, configure the allowed block types, and editors compose the entry's content from those blocks. This is useful when entries need flexible, section-based content rather than a fixed set of fields. --- ## Nested Blocks Blocks can contain other blocks. A block model can include a block field that references other block types, creating a hierarchical content structure where sections contain sub-sections. This enables complex but governed layouts. A "Section" block might allow "Card" blocks inside it, creating a grid of cards within a section. A "Tabs" block might allow "Tab Panel" blocks, each containing its own content. A "Two Column" block might allow different block types in each column. ``` Page └── Dynamic Block Field ├── Hero Block │ └── Fields (heading, image, CTA) ├── Features Section Block │ └── Block Field → Feature Card Blocks │ ├── Feature Card (icon, title, description) │ ├── Feature Card (icon, title, description) │ └── Feature Card (icon, title, description) └── Tabs Block └── Block Field → Tab Panel Blocks ├── Tab Panel (label, content) └── Tab Panel (label, content) ``` Nesting depth is up to you, but keep the editor experience in mind. Two levels of nesting — a section containing cards — is intuitive. Three or four levels can make the editing interface difficult to navigate. Design your block architecture to balance structural richness with editorial usability. --- ## Block Composability A single block model can be used across multiple page models, multiple entry models, or both. This is one of the most powerful aspects of the block system — you define a component once and reuse it everywhere. A "CTA" block model with fields for heading, body text, button label, and button URL can appear on the homepage, on blog posts, on product pages, and on landing pages. The block's structure and template are defined once. Any page model that allows the CTA block gives editors access to it. ### Structural Changes Propagate When you modify a block model's structure — adding a field, removing a field, or changing a field type — the change applies everywhere that block is used. If you add a "background color" field to the CTA block, every page and entry that includes CTA blocks gains that field in its editor. This is a design system principle applied to content. Block models act as your content components, and changes to the component definition propagate across the entire site. It means you maintain content structures in one place rather than updating the same field set on every model that uses it. ### Building a Block Library As your site grows, your block models form a library of content components. A well-designed block library might include: **Layout blocks** — Hero, two-column, three-column, full-width section, sidebar layout. These define spatial arrangement. **Content blocks** — Rich text section, image with caption, video embed, blockquote. These carry primary content. **Interactive blocks** — Accordion, tabs, carousel, before/after slider. These provide interactive patterns. **Conversion blocks** — CTA banner, pricing table, contact form, newsletter signup. These drive user action. **Social proof blocks** — Testimonial, client logos, case study highlight, statistics counter. These build credibility. Not every site needs all of these. Start with the blocks your content requires and add new block models as needs emerge. The block library grows naturally as you identify repeating patterns in your pages. --- ## View Template Separation Each block model has its own EJS template in the site builder. This is a key architectural decision — the rendering logic for a block lives with the block, not with the pages that use it. When the site builder compiles a page that contains dynamic blocks, it renders each block instance using the block's own template and injects the output into the page. The page template doesn't need to know how to render a hero banner or a pricing table — it delegates that to the block templates. This separation has practical benefits. Changing how a testimonial block renders only requires editing the testimonial block template. The change takes effect everywhere the block appears, across all pages and entries. You don't need to find and update rendering logic in every page template that might include testimonials. Block templates receive the block's field data as the `item` variable, just like page and entry templates. They also have access to the full site content, so a block template can reference entries, images, options, or other data beyond what's stored in the block's own fields. → [Model Templates](/builder/code/main) → [Template Context and Data Access](/builder/code/context) --- ## When to Use Block vs Group Blocks and groups both organize fields into logical units, but they serve different purposes and have different capabilities. Choosing between them depends on whether you need reusability and composability or just field organization. **Use a block when:** - The component appears across multiple models. A CTA section used on the homepage, blog posts, and landing pages should be a block — define it once, use it everywhere. - Editors need to add, remove, reorder, or hide instances. Dynamic block fields give editors composition control that groups don't provide. - The component has its own visual template. Blocks get their own EJS template in the site builder, making rendering modular and maintainable. - You want structural changes to propagate. Modifying a block model updates every instance across the site. **Use a group when:** - The fields are specific to one model and won't be reused elsewhere. An SEO metadata group on a page model doesn't need to be a block. - The structure is fixed — editors shouldn't add or remove instances. Groups are static containers; their fields always appear in the editor. - You just need visual organization in the editor. Groups let you cluster related fields (e.g., address fields, social links) without the overhead of a separate model. The distinction is straightforward: blocks are reusable components, groups are organizational containers. If you find yourself duplicating the same set of fields across multiple models, that's a signal to extract them into a block model. | Capability | Block | Group | |---|---|---| | Reusable across models | Yes | No | | Own template in site builder | Yes | No | | Editors can add/remove/reorder | Yes (via dynamic block field) | No | | Changes propagate across site | Yes | No | | Nesting support | Yes | Yes | | Best for | Composable sections, UI components | Field organization within a model | --- ## Block Models and the Content API In API responses, blocks appear as objects within a dynamic block field array. Each block object includes a type identifier and the block's field data, giving your frontend the information it needs to select the right component for rendering. ```typescript const page = client.getPage('/'); // page.sections → [ // { _type: 'hero', heading: 'Welcome', image: { url: '...' }, cta: { ... } }, // { _type: 'features', items: [...] }, // { _type: 'testimonials', quotes: [...] }, // { _type: 'cta', heading: 'Get Started', buttonLabel: 'Sign Up', buttonUrl: '/signup' } // ] ``` Your frontend iterates over the block array and renders the appropriate component for each type. In a React application, this typically maps to a component lookup: ```tsx const blockComponents = { hero: HeroSection, features: FeaturesGrid, testimonials: TestimonialCarousel, cta: CTABanner, }; page.sections.map(block => { const Component = blockComponents[block._type]; return Component ? : null; }); ``` Nested blocks appear as nested arrays within the parent block's data, following the same structure recursively. → [Content API](/publish/api) → [@sleekcms/client](/publish/api/client) --- ## What's Next - [Page Models](/model/pages) — Routable content types with URL paths and slug-based collections. - [Entry Models](/model/entries) — Structured, non-routable content for shared data like authors, categories, and settings. - [Content Field Types](/model/fields) — The full set of field types available in page, entry, and block models. - [Dynamic Blocks](/model/fields/dynamic) — Deep dive into configuring dynamic block fields and the editor experience. - [Group Fields](/model/field/group) — Organizing fields within a model without the overhead of a separate block. - [Content Editing](/content) — The editing experience for content creators working with blocks. An option set is a predefined list of key-value pairs that powers dropdown fields in your content models. When you need editors to choose from a fixed set of values — a color theme, a layout variant, a content category — you define the choices as an option set and connect it to a dropdown field. This page covers how option sets work, how they connect to dropdown fields, accessing option sets through the content API, and how to decide between option sets and entry models for taxonomy. --- ## What Is an Option Set An option set is a simple list of label-value pairs managed at the site level. Each pair consists of a human-readable label that editors see in the dropdown and a machine-readable value that gets stored and delivered through the API. For example, a "Button Style" option set might contain: | Label | Value | |---|---| | Primary | `primary` | | Secondary | `secondary` | | Outline | `outline` | | Ghost | `ghost` | The editor sees "Primary", "Secondary", "Outline", and "Ghost" in the dropdown. The stored value — what your templates and frontend code receive — is `primary`, `secondary`, `outline`, or `ghost`. Option sets are defined once and can be used by dropdown fields across any number of models. The "Button Style" set can power a dropdown on a CTA block, a hero block, a card block, and anywhere else you need a button style choice. Changes to the option set — adding a new style, renaming a label, removing an option — are reflected everywhere the set is used. --- ## Key-Value Pairs Each item in an option set is a pair: a **label** and a **value**. **Labels** are for editors. They appear in the dropdown UI and should be clear, descriptive, and written in natural language. "Two Columns", "Dark Background", "Left Aligned" — these are labels. **Values** are for code. They're stored in the content record and delivered through the API. Values are typically lowercase, hyphenated or snake_cased strings that your templates and frontend code use for logic and styling: `two-columns`, `dark-bg`, `left`. Keeping labels and values separate means you can rename what editors see without breaking your templates. If you change the label from "Two Columns" to "Side by Side", the stored value `two-columns` stays the same and nothing in your code needs to change. --- ## Using Option Sets in Dropdown Fields To use an option set, you add a dropdown field to a model and connect it to the option set. The field then presents the option set's labels as choices in the editor. A dropdown field connected to an option set stores the selected value (not the label) in the content record. When an editor selects "Primary" from a button style dropdown, the content record stores `primary`. When your template or frontend reads that field, it gets the string `primary` and can use it for CSS classes, conditional rendering, or any other logic. Dropdown fields can be added to page models, entry models, and block models. This makes option sets available anywhere you need a constrained choice — layout variants on a block, content categories on a page, theme selections on a site settings entry. ### Common Uses **Layout control** — Let editors choose between layout variants for a block. A features section might offer "grid", "list", or "carousel" layouts. The block template reads the selected value and renders the appropriate layout. **Theme and styling** — Let editors select color themes, background styles, or spacing options. A section block might offer "light", "dark", or "brand" themes, and the template applies the corresponding CSS class. **Content classification** — Let editors tag content with a type or category. A blog post might have a "Post Type" dropdown with values like "article", "tutorial", "announcement", or "case-study". **Configuration flags** — Let editors toggle behavior through predefined options. A CTA block might have a "Size" dropdown with "compact" and "full-width" values, or an "Animation" dropdown with "none", "fade-in", and "slide-up". --- ## Centralized Taxonomy Option sets serve as a lightweight taxonomy system. When you need a consistent set of categories, tags, or classifications across your site, defining them as an option set ensures editors always choose from the same list. Without option sets, you'd rely on free-text fields, which inevitably leads to inconsistency — one editor types "Case Study", another types "case-study", a third types "Case study". A dropdown backed by an option set eliminates this problem. The values are predefined, consistent, and controlled. Because option sets are managed centrally, adding a new taxonomy value — a new category, a new tag, a new classification — is a single change that immediately appears in every dropdown connected to that set. You don't need to update each model individually. --- ## Accessing Option Sets via API Option sets with a handle can be retrieved directly through the content API using the `getOptions()` method. This is useful when your frontend needs the full list of options — for building filter interfaces, rendering navigation based on categories, or populating client-side dropdowns. To make an option set available through the API, assign it a handle in the admin UI. The handle is the identifier you pass to `getOptions()`. ```typescript const categories = client.getOptions('categories'); // [ // { label: 'Technology', value: 'tech' }, // { label: 'Design', value: 'design' }, // { label: 'Business', value: 'business' }, // { label: 'Marketing', value: 'marketing' } // ] ``` This is separate from how option values appear in content records. When a page or entry has a dropdown field, the API response for that record includes the selected value as a string. The `getOptions()` method gives you the complete list of available options, which is what you need when you want to render all choices rather than just the one that was selected. A practical example: a blog has a "Category" dropdown backed by a "Categories" option set. Individual blog posts return their selected category value in the API response (`post.category → 'design'`). But your category filter sidebar needs the full list of categories to render all the filter buttons. That's where `getOptions('categories')` comes in. ### In Templates In the site builder, option sets are accessible through the template context. You can iterate over an option set to render navigation, filter lists, or any UI that needs the full set of choices. The selected value on a content record is available through the `item` variable, while the complete option set is available through the global `options` accessor. → [Content API](/publish/api) → [Template Context and Data Access](/builder/code/context) --- ## Option Set vs Entry Model Option sets and entry models can both serve as sources of taxonomy or categorization, but they solve different problems. The choice depends on the complexity of the data and how it's used. **Use an option set when:** - The data is a simple label-value pair with no additional fields. A list of color themes, layout options, or content categories where each item is just a name and a code. - The list is relatively stable and managed by administrators. Option sets are configuration, not content — they define the available choices, not something editors create on the fly. - You need a dropdown in the editor. Option sets are designed to power dropdown fields with minimal setup. **Use an entry model when:** - Each item needs additional fields beyond a label and value. A "Category" with a name, description, icon, and featured image is too complex for an option set — it needs its own model with multiple fields. - Editors need to create and manage items independently. Entry models appear in the content list where editors can add, edit, and delete entries. Option sets are managed in the modeling layer, typically by administrators. - The data will be referenced with its full structure. Entry references resolve to the complete entry object in API responses, giving your frontend access to all fields. Option set values are just strings. - Items need their own visual representation. Entries can have templates in the site builder. Option sets are purely data. | Capability | Option Set | Entry Model | |---|---|---| | Data structure | Label + value only | Multiple typed fields | | Managed by | Administrators (modeling layer) | Content editors (content list) | | Used in editor as | Dropdown selection | Reference field selection | | API response shape | String value | Full object with all fields | | Own template in site builder | No | Yes | | Best for | Simple choices, flags, variants | Rich taxonomy, complex relationships | In practice, many sites use both. Option sets handle simple choices like layout variants, button styles, and theme selections. Entry models handle rich taxonomy like authors, categories with descriptions, and any classification that carries its own content. --- ## What's Next - [Content Field Types](/model/fields) — The full set of field types including dropdown, reference, and other relational fields. - [Page Models](/model/pages) — Routable content types that use option sets in dropdown fields. - [Entry Models](/model/entries) — Structured content for rich taxonomy and reusable data. - [Block Models](/model/blocks) — Composable content components where option sets control layout and style variants. - [Content API](/publish/api) — REST and GraphQL endpoints for retrieving option sets and content. Fields are the building blocks of every model in SleekCMS. When you add a field to a page model, entry model, or block model, you're defining a single piece of structured data — a title, an image, a date, a layout choice. The fields you choose determine what editors fill in, how content is validated, and what shape your API responses take. This page covers every field type available in SleekCMS, organized by category. Each field type includes what it stores, how editors interact with it, and how it appears in the content API. --- ## Field Type Reference | Field | Category | Stores | Multiple | Notes | |---|---|---|---|---| | Text | Textual | String or string array | Yes | Single line or textarea. Regex, min/max validation. | | Number | Textual | Number | No | Numeric input. | | Markdown | Textual | HTML string or array | Yes | Rendered to HTML in API response. | | Rich Text | Textual | HTML string or array | Yes | Visual editor with formatting controls. | | Boolean | Textual | Boolean | No | Toggle switch. | | Dropdown | Choice | String | No | Backed by an option set. | | Color | Choice | String | No | Color picker. | | Date | Temporal | String (`YYYY-MM-DD`) | No | Date picker. | | Date Time | Temporal | String (ISO 8601) | No | Date and time picker. | | Image | Media | Object or object array | Yes | Gallery, upload, Unsplash, Pexels, Iconify, Pixabay, or URL. | | Video | Media | String | No | External video URL. | | JSON | Data | Object or array | No | Raw JSON editor. | | Code | Data | String | No | Syntax-highlighted code editor. | | Location | Data | Object or object array | Yes | Coordinates with static Google Map link. | | Data Table | Data | Table structure | No | Freeform or fixed columns/rows. | | Reference | Relational | Object or object array | Yes | Links to entry records. Full object in API. | | Link | Relational | Object | No | Page path or external URL. Resilient to path changes. | | Block | Structural | Object | No | Single block instance from a block model. | | Dynamic Blocks | Structural | Array of objects | — | Composable stack of block instances. | | Group | Structural | Object | No | Organizational container for fields. | | Collection | Structural | Array of objects | — | Repeatable group of fields. | --- ## Textual Fields Textual fields store written content — from a single-line title to a full rich text article body. ### Text A text field stores a string value. It can be configured as a single-line input or a multiline textarea, depending on whether the content is a short label or a longer passage like a description or excerpt. Text fields support validation rules: regular expression patterns for format enforcement, minimum and maximum character counts, and minimum and maximum word counts. A "Phone Number" text field might use a regex pattern to ensure consistent formatting. A "Meta Description" field might enforce a maximum of 160 characters. When configured as **multiple**, the field accepts more than one value and returns an array of strings in the API. This is useful for tags, keywords, or any field where editors need to provide a variable number of short text values. ### Number A number field stores a numeric value. Use it for quantities, prices, sort orders, ratings, durations, or any content that is inherently numerical. ### Markdown A markdown field provides a text editor where content is written in Markdown syntax. The API returns the content rendered as HTML, so your frontend receives ready-to-display markup without needing a Markdown parser on the client. When configured as **multiple**, the field returns an array of HTML strings. This is useful when a model needs several independent Markdown content sections — for example, multiple answer fields on an FAQ entry. ### Rich Text A rich text field provides a visual formatting toolbar — bold, italic, links, lists, headings — similar to a word processor. Editors work with formatted text directly rather than writing markup. When configured as **multiple**, the field returns an array of HTML strings. Like Markdown, this supports models that need several independent formatted content areas. ### Boolean A boolean field stores a true/false value, presented to editors as a toggle switch. Use it for visibility flags, feature toggles, or any binary choice — "Show author bio", "Pin to top", "Enable comments". --- ## Choice Fields Choice fields let editors select from predefined options rather than entering free-form values. ### Dropdown A dropdown field presents a list of choices from a connected option set. Editors select one value, and the selected option's value (not its label) is stored in the content record. Dropdowns are the primary consumer of option sets. A "Layout" dropdown connected to a layout option set might offer "grid", "list", and "carousel". A "Theme" dropdown might offer "light", "dark", and "brand". → [Option Sets and Lists](/model/options) ### Color A color field provides a color picker interface and stores the selected color as a string value. Use it for background colors, accent colors, text colors, or any field where editors need to choose a specific color. --- ## Temporal Fields Temporal fields store dates and times. ### Date A date field provides a date picker and stores the value in `YYYY-MM-DD` format. Use it for publish dates, event dates, deadlines, or any field that represents a calendar day without a specific time. ### Date Time A date time field provides both a date and time picker and stores the value in ISO 8601 format. Use it for event start times, scheduled publish times, or any field where the specific time of day matters. --- ## Media Fields Media fields handle images, video, and other visual content. ### Image An image field stores an image reference with metadata. Editors can select images from multiple sources: the site's media gallery, local file upload, Unsplash, Pexels, Iconify, Pixabay, or by providing a direct URL. When configured as **multiple**, the field accepts several images and returns an array of image objects. This is useful for photo galleries, product image carousels, or any field that needs more than one image. ### Video A video field stores an external video URL — typically a YouTube, Vimeo, or other hosted video link. It does not handle direct video uploads. Use it for embedded video content, video backgrounds, or any field that references a hosted video. --- ## Data Fields Data fields store structured or specialized data that doesn't fit the textual or media categories. ### JSON A JSON field provides a raw JSON editor for storing arbitrary structured data. Use it for configuration objects, structured metadata, or any data that doesn't map cleanly to other field types. The API returns the stored JSON as a parsed object or array. ### Code A code field provides a syntax-highlighted code editor. Use it for embedding code snippets, storing template fragments, or any field where the content is source code. The API returns the code as a string. ### Location A location field stores geographic coordinates. The API response includes the coordinates along with a link to a static Google Map image, whose parameters (size, zoom, style) are customizable. When configured as **multiple**, the field accepts several locations and returns an array of location objects. This is useful for store locators, event venues, or any content with multiple geographic points. ### Data Table A data table field provides a spreadsheet-like editing interface for tabular content. It can be configured in three modes: **Freeform** — Editors control both the columns and rows. They can add, remove, and rename columns and add as many rows as needed. Use this when the table structure varies per content instance. **Fixed columns** — You define the column headers in the model, and editors add rows of data. Use this when every instance should have the same columns but a variable number of rows — a pricing comparison, a specification list, a schedule. **Fixed columns and rows** — You define both the columns and the rows in the model, and editors fill in the cell values. Use this when the entire table structure is predetermined — a fixed comparison matrix or a standard form. --- ## Relational Fields Relational fields create connections between content — linking pages to entries, entries to other entries, or content to navigation paths. ### Reference A reference field links to one or more records from an entry collection. When an editor adds a reference field to a model, they configure which entry model it references. In the editor, they select from a dropdown of available entries from that collection. A one-to-one reference links to a single entry. The API returns the full entry object inline — all of the referenced entry's fields are embedded in the response, so your frontend has immediate access without a separate API call. A one-to-many reference links to multiple entries. The API returns an array of full entry objects. Use this for categories, tags, related items, contributors, or any relationship where one piece of content connects to several entries. → [Entry Models](/model/entries) ### Link A link field stores a navigation reference — either a path to a page within your site or an external URL. Editors can select from existing page paths or enter an arbitrary URL for external links. Page path links are resilient to path changes. If you update a page model's path from `/about` to `/about-us`, any link field that references that page automatically reflects the new path. This prevents broken internal links when restructuring your site's URL hierarchy. --- ## Structural Fields Structural fields organize other fields into groups, repeatable sets, and composable layouts. They don't store content themselves — they define how other fields are arranged. ### Block A block field embeds a single block instance from a specified block model. Unlike a dynamic block field where editors choose from multiple block types, a block field is tied to one block model and always renders that specific block. Use block fields when a section of the page should always be a specific block type — a page model that always includes a hero block at the top, or an entry that always includes a metadata block. → [Block Models](/model/blocks) ### Dynamic Blocks A dynamic block field is the composable layout mechanism in SleekCMS. It lets editors build a section of content by adding, removing, reordering, and hiding block instances from a set of allowed block types. When configuring a dynamic block field, you control which blocks editors can choose from with three selection modes: **All blocks** — Every block model in the site is available to editors. Use this for maximum flexibility, typically on general-purpose landing page models. **Blocks with prefix** — Only block models whose handle starts with a specific prefix are available. This lets you organize blocks by convention — all blocks prefixed with `blog-` are available on blog post models, all blocks prefixed with `landing-` are available on landing pages. Adding a new block with the matching prefix automatically makes it available without reconfiguring the field. **Selected blocks** — You explicitly choose which block models are available. This gives you the tightest control over what editors can add, and is the most common configuration for models where layout governance matters. → [Block Models](/model/blocks) → [Dynamic Blocks](/model/fields/dynamic) ### Group A group field is an organizational container that nests related fields together. Groups appear as collapsible sections in the editor, keeping the interface structured when a model has many fields. A page model with SEO fields (meta title, meta description, Open Graph image) might group them under an "SEO" group. A block model with CTA fields (button label, button URL, button style) might group them under a "Button" group. Groups affect the editor layout and the data nesting in the API response, but they're purely organizational — they don't create independent content that can be reused. Fields within a group can be regrouped or moved to a different group without losing existing content. The data stays intact; only the editorial organization changes. ### Collection A collection field is a repeatable group of fields — editors can add multiple items, each with the same set of sub-fields. Think of it as an inline list where each item has a structured shape. Common uses include FAQ lists (question + answer pairs), feature lists (icon + title + description), timeline entries (date + event + description), and any content where editors need a variable number of structured items. Collections are similar to blocks in that they produce arrays of structured objects, but they differ in important ways. Collections are defined inline within the model — their field structure is specific to that model and can't be reused elsewhere. Blocks are defined as separate models and can be shared across many page and entry models. If you need the same repeating structure in multiple models, use a block. If the structure is specific to one model, a collection keeps things simple. --- ## What's Next - [Group Fields](/model/field/group) — Deep dive into organizing fields with groups and column layouts. - [Collection Fields](/model/field/collection) — Repeatable inline structures and when to use them vs blocks. - [Block Fields](/model/fields/block) — Using single block and dynamic block fields in your models. - [Dynamic Blocks](/model/fields/dynamic) — Configuring allowed blocks, the editor experience, and layout governance. - [Option Sets](/model/options) — Defining the key-value pairs that power dropdown fields. - [Page Models](/model/pages) — How fields combine to define routable page content. - [Entry Models](/model/entries) — How fields combine to define reusable structured data. A group field is an organizational container that nests related fields into a single object. Groups don't store content themselves — they structure how fields are arranged in the editor and how data is nested in the API response. When a model has many fields, groups turn a flat list into a logical hierarchy that editors can navigate and developers can consume as nested JSON. This page covers how groups work, column layouts for horizontal field arrangement, nesting groups inside groups, moving fields between groups without data loss, and how to decide between groups and other structural field types. --- ## What Is a Group Field A group field wraps a set of fields into a named, collapsible section in the editor. The fields inside a group are part of the same model — they aren't separate content that can be reused elsewhere. A group is purely organizational: it affects how editors see the fields and how the data is structured in the API response. When you add a group field to a model, you configure: **A name and handle** — The name is what editors see as the section heading in the editor. The handle determines the key name in the API response where the grouped fields are nested. **Fields** — The fields that belong to the group. Any field type can be placed inside a group — text, images, references, collections, even other groups. The fields inside a group work exactly the same as fields at the top level of a model; grouping doesn't change their behavior or capabilities. **Column layout** — The number of columns used to arrange fields horizontally within the group. This controls the visual layout of the editor, letting you place related fields side by side rather than stacking them vertically. A group always produces exactly one instance. Unlike collections, which are repeatable, or dynamic block fields, which let editors add multiple sections, a group is a fixed container. Its fields always appear in the editor, and editors fill them in once. You can't add a second instance of a group or remove it from the editor — it's a permanent part of the model's structure. --- ## Organizing Fields with Groups Groups solve the problem of editor complexity. A page model with 15 fields presented as a flat list is harder to navigate than one where those fields are organized into three or four named sections. Groups give you that structure. ### Common Grouping Patterns **SEO and metadata** — Group meta title, meta description, Open Graph image, and canonical URL under an "SEO" group. Editors working on content can collapse this section until they need it, and developers receive the metadata as a clean nested object. **Hero or banner section** — Group a heading, subheading, background image, and CTA button fields under a "Hero" group. This clusters visually related fields and produces a single `hero` object in the API. **Contact information** — Group address, phone, email, and map location fields under a "Contact" group on a location page model. The editor sees one coherent section, and the API returns a `contact` object with all the details. **Social links** — Group social media URLs (Twitter, LinkedIn, Instagram, YouTube) under a "Social" group on a site settings entry. Editors see them as a logical unit, and your template or frontend accesses them through `settings.social`. **Button or CTA fields** — Group a button label, button URL, and button style dropdown under a "Button" group inside a block model. The block template accesses the button data as `item.button.label`, `item.button.url`, and `item.button.style`. The pattern is consistent: whenever you have a set of fields that belong together conceptually, a group makes both the editing interface and the data structure cleaner. --- ## Column Layouts By default, fields inside a group stack vertically — one field per row, full width. Column layouts let you arrange fields horizontally, placing two, three, or four fields side by side in a single row. When you configure a group, you select the number of columns. Fields within the group are then distributed across those columns in order, flowing left to right. This is an editor layout feature — it affects how fields are displayed in the editing interface, not how the data is structured in the API. ### When to Use Columns Columns work well when fields are short and logically paired. A "Name" group with first name and last name fields makes sense as two columns — editors see both fields on the same row, which reflects how they think about the data. A "Dimensions" group with width, height, and unit fields works as three columns. Some practical examples: **Two columns** — First name and last name. City and country. Start date and end date. Min price and max price. These are natural pairs where side-by-side layout improves scannability. **Three columns** — Icon, title, and description for a compact feature entry. Width, height, and depth for product dimensions. **Four columns** — Margin or padding values (top, right, bottom, left). Social media URLs where each is a short text field. Avoid using columns for fields that need the full editor width, like rich text editors, markdown fields, or long text areas. These are better left at full width where editors have room to work with the content. Columns are most effective for short text fields, numbers, dropdowns, booleans, and images. --- ## Nesting Groups Groups can contain other groups, creating a hierarchical data structure. A top-level "Hero" group might contain a "Background" sub-group with image, overlay color, and opacity fields, plus a "Content" sub-group with heading, subheading, and CTA fields. In the editor, nested groups appear as collapsible sections within their parent group. In the API response, they produce nested objects: ```json { "hero": { "background": { "image": { "url": "..." }, "overlay_color": "#000000", "opacity": 0.5 }, "content": { "heading": "Welcome", "subheading": "Get started today", "cta_label": "Sign Up" } } } ``` Nesting is useful when a group contains enough fields that further subdivision improves clarity. Keep nesting shallow — one or two levels deep is typical. Deeply nested groups make both the editor and the data structure harder to navigate. --- ## Data Structure in the API Groups create nested objects in the API response. A field placed inside a group is accessed through the group's handle as a parent key, rather than at the top level of the record. A page model with a title field at the top level and an SEO group containing meta_title and meta_description produces this API shape: ```json { "title": "About Us", "seo": { "meta_title": "About Our Company | Brand", "meta_description": "Learn about our mission and team." } } ``` Without the group, all three fields would be at the top level. With the group, the SEO fields are nested under the `seo` key. Your frontend accesses them as `page.seo.meta_title` and `page.seo.meta_description`. This nesting is reflected in templates as well. In the site builder, you access grouped fields through the group handle: ```ejs <%= item.seo.meta_title %> ``` And through the content API client: ```typescript const page = client.getPage('/about'); console.log(page.seo.meta_title); // "About Our Company | Brand" ``` --- ## Moving and Ungrouping Fields Fields within a group can be moved to a different group or ungrouped entirely — pulled back to the top level of the model — without losing existing content. This is a key property of groups: they organize data, but the underlying field data is preserved regardless of how you reorganize the grouping structure. This means you can restructure your editor layout as your model evolves. If you start with a flat model and later decide to group SEO fields together, you can create a group and move the existing fields into it. The content editors have already entered is retained. If you later decide to split one group into two, or dissolve a group and move its fields elsewhere, the data follows the fields. The practical implication is that groups are low-risk to add, change, or remove. You're not locked into a grouping structure once content exists. This is different from blocks, where changing the block model's structure affects all instances across the site. Groups are local to the model they belong to, and reorganizing them is a non-destructive operation. Note that while the content data is preserved, the nesting structure in the API response does change when you move fields between groups. If your frontend accesses `page.seo.meta_title` and you move `meta_title` out of the `seo` group, the API path changes to `page.meta_title`. Your templates or frontend code will need to be updated to reflect the new structure. --- ## When to Use Group vs Block Groups and blocks both organize fields, but they serve fundamentally different purposes. The decision depends on whether you need reusability and composability or just editorial organization. **Use a group when:** - The fields are specific to one model. An SEO group on a blog post model doesn't need to exist as a separate, reusable component. - The structure is fixed. Every page of this model should have these fields — editors can't add or remove groups. - You want to organize the editor visually. Groups are a zero-overhead way to cluster related fields and reduce visual clutter. - You need horizontal column layouts. Groups give you column control for arranging fields side by side. **Use a block when:** - The same set of fields appears across multiple models. A CTA with heading, button label, and button URL used on five different page models should be a block — define it once, use it everywhere. - Editors need to add, remove, or reorder instances. Blocks inside dynamic block fields give editors composition control. - The component has its own visual template. Blocks get their own EJS template in the site builder. - You want structural changes to propagate site-wide. Modifying a block model updates every instance. A useful heuristic: if you're duplicating the same set of fields across multiple models, extract them into a block. If the fields are unique to one model and just need better organization, use a group. --- ## When to Use Group vs Collection Groups and collections both nest fields, but groups produce a single object while collections produce a repeatable array. **Use a group when** the structure appears once per record. A page has one set of SEO fields, one hero section, one contact details block. The data is singular — there's no "add another" action for editors. **Use a collection when** editors need multiple items with the same structure. An FAQ page needs multiple question-answer pairs. A feature list needs multiple icon-title-description items. The data is plural — editors add as many items as they need. The API response makes the distinction clear. A group produces a single nested object. A collection produces an array of objects: ```json { "seo": { "meta_title": "...", "meta_description": "..." }, "faqs": [ { "question": "...", "answer": "..." }, { "question": "...", "answer": "..." } ] } ``` `seo` is a group — one object, always present. `faqs` is a collection — an array with zero or more items. --- ## What's Next - [Collection Fields](/model/field/collection) — Repeatable inline structures for variable-length lists. - [Block Models](/model/blocks) — Reusable, composable content components for dynamic page sections. - [Dynamic Blocks](/model/fields/dynamic) — Configuring allowed blocks, the editor experience, and layout governance. - [Content Field Types](/model/fields) — The full set of field types available in page, entry, and block models. - [Page Models](/model/pages) — How fields combine to define routable page content. - [Entry Models](/model/entries) — How fields combine to define reusable structured data. A collection field is a repeatable group of fields — a structure where editors can add multiple items, each with the same set of sub-fields. Think of it as an inline list where every item has a defined shape. An FAQ section with question-answer pairs, a feature list with icon-title-description rows, a timeline with date-event entries — these are collections. The structure is defined once in the model, and editors create as many items as they need. This page covers how collections work, the editing experience for adding and managing items, how collection data appears in the API and templates, nesting collections and groups, and how to decide between collections and other structural field types. --- ## What Is a Collection Field A collection field defines a repeatable set of sub-fields within a model. Each item in the collection has the same field structure, but editors control how many items exist. A model with a collection field is saying: "this content has a variable-length list of structured items." When you add a collection field to a model, you configure: **A name and handle** — The name is what editors see as the section heading in the editor. The handle determines the key name in the API response where the array of items is stored. **Sub-fields** — The fields that make up each item. Any field type can be used inside a collection — text, images, numbers, dropdowns, references, groups, even other collections. You define the fields once, and every item in the collection has that same structure. The result is an array of structured objects. A "Features" collection with sub-fields for icon, title, and description produces an array where each element has those three fields. Editors add items to the array, fill in the sub-fields for each one, and reorder or remove items as needed. Collections are defined inline within the model they belong to. Unlike blocks, which are defined as separate models and can be shared across many page and entry models, a collection's field structure is specific to its parent model. This makes collections simpler to set up — there's no separate model to create — but it means the structure can't be reused elsewhere. --- ## Common Use Cases Collections appear wherever content naturally takes the form of a list with a consistent structure. A few patterns that come up frequently: **FAQ lists** — A collection with question and answer fields. Editors add one item per FAQ entry, and the result is an array of Q&A pairs that your frontend renders as an accordion, a list, or any layout you choose. **Feature lists** — A collection with icon, title, and description fields. Each item represents a feature, benefit, or selling point. This is common on landing pages, product pages, and pricing sections. **Social links** — A collection with icon and URL fields. Each item represents a social media profile. This is a typical pattern on footer or header entry models where the number of social links varies per site. **Timeline or history entries** — A collection with date, title, and description fields. Each item is a milestone, event, or historical entry displayed in chronological order. **Team member highlights** — A collection with name, role, photo, and bio fields on an "About" page model, when team members don't need their own URLs or entry records. If team members are referenced from multiple places, an entry model is a better fit. **Navigation items** — A collection with label, URL, and optional icon fields on a navigation entry model. Each item is a menu link. Nested navigation can be modeled with collections inside collections. **Pricing tiers** — A collection with plan name, price, description, and features sub-fields. Each item is a tier on a pricing table. **Testimonials or quotes** — A collection with quote text, author name, author title, and company fields. Each item is a testimonial that renders as a card, carousel slide, or list item. The unifying characteristic is a variable number of items with a consistent structure. If every item has the same fields and editors need to control how many items exist, a collection is the right tool. --- ## The Editor Experience The collection editor presents items as a vertical list where each item can be expanded to reveal its sub-fields. Editors interact with collections through a straightforward set of actions. ### Adding Items Editors add a new item to the collection and fill in its sub-fields. The new item appears at the end of the list with empty fields ready for content. There's no limit on how many items an editor can add — a collection grows as needed. ### Reordering Items Items can be reordered by dragging them to a new position. If the fifth FAQ entry should be first, the editor drags it to the top. The order of items in the editor is the order they appear in the API response and in templates. ### Removing Items Editors can remove an item from the collection. This deletes the item and its content permanently. Unlike dynamic blocks, there is no "hide" action — removing is the only way to exclude an item. ### Inline Editing Each item's sub-fields are edited directly within the collection interface. Editors expand an item to see its fields, make changes, and collapse it. There's no separate editing screen or modal — everything happens inline within the parent model's editor. For collections with many sub-fields per item, the interface keeps things manageable through collapsible item sections. Editors can collapse items they've already completed and focus on the one they're currently editing. --- ## Data Structure in the API A collection field produces an array of objects in the API response. Each object in the array contains the sub-fields defined for that collection, and the array order matches the order editors set in the editor. A page model with a "Features" collection containing icon, title, and description sub-fields produces this shape: ```json { "title": "Our Product", "features": [ { "icon": "⚡", "title": "Lightning Fast", "description": "Built for speed from the ground up." }, { "icon": "🔒", "title": "Secure by Default", "description": "Enterprise-grade security out of the box." }, { "icon": "🔌", "title": "Easy Integration", "description": "Connect to your existing tools in minutes." } ] } ``` The `features` array has three items because the editor added three items. A different page instance using the same model might have five items or one. The structure of each item is identical — icon, title, description — but the number of items varies per record. Empty collections return an empty array. If an editor creates a page but doesn't add any items to the collection, the API returns `"features": []`. ### Nested Structures Collections can contain groups, and groups can contain collections. This lets you model more complex data structures when items have sub-sections of their own. A "Pricing Tiers" collection might contain a group for "Plan Details" (name, price, billing period) and a nested collection for "Included Features" (feature name, included boolean). The API response reflects the nesting: ```json { "pricing_tiers": [ { "plan": { "name": "Starter", "price": 9, "period": "month" }, "included_features": [ { "feature": "5 projects", "included": true }, { "feature": "API access", "included": false } ] }, { "plan": { "name": "Pro", "price": 29, "period": "month" }, "included_features": [ { "feature": "Unlimited projects", "included": true }, { "feature": "API access", "included": true } ] } ] } ``` Nesting is powerful but adds editorial complexity. A collection inside a collection means editors are managing lists within lists. Keep nesting shallow — one level of nesting is usually sufficient. If the structure gets deeper, consider whether blocks or entry references would simplify the editing experience. --- ## Collections in Templates In the site builder, collections are accessed through the `item` variable and iterated with standard EJS loops. The template receives the collection as an array and renders each item. ### Basic Iteration A features collection rendered as a grid: ```ejs
<% item.features.forEach(feature => { %>
<%= feature.icon %>

<%= feature.title %>

<%= feature.description %>

<% }); %>
``` ### FAQ Accordion A collection of question-answer pairs rendered as an accordion: ```ejs
<% item.faqs.forEach((faq, index) => { %>
<%= faq.question %>
<%- faq.answer %>
<% }); %>
``` ### Social Links A collection of social media links rendered in a footer: ```ejs ``` ### Conditional Rendering Check whether the collection has items before rendering its container: ```ejs <% if (item.features && item.features.length > 0) { %>

Features

<% item.features.forEach(feature => { %>
<%= feature.title %>
<% }); %>
<% } %> ``` --- ## Collections in the Content API When consuming collections through the `@sleekcms/client`, you access them as arrays on the page or entry object. Your frontend iterates over the array and renders the appropriate UI for each item. ```typescript const page = client.getPage('/pricing'); // Iterate over a collection page.pricing_tiers.forEach(tier => { console.log(tier.plan.name, tier.plan.price); tier.included_features.forEach(f => { console.log(` ${f.feature}: ${f.included ? '✓' : '✗'}`); }); }); ``` In a React application, collections map naturally to list rendering: ```tsx function FeatureList({ features }) { return (
{features.map((feature, index) => (
{feature.icon}

{feature.title}

{feature.description}

))}
); } // Usage const page = await client.getPage('/product'); ``` → [Content API](/publish/api) → [@sleekcms/client](/publish/api/client) --- ## When to Use Collection vs Block Collections and blocks both produce arrays of structured objects, but they differ in scope, reusability, and the editor experience. Choosing between them depends on whether the repeating structure is local to one model or shared across many. **Use a collection when:** - The repeating structure is specific to one model. An FAQ list on a product page model, a list of social links on a footer entry — these don't need to be shared across other models. - The items are simple and uniform. Each item has the same few fields, and there's no need for different item types within the same list. - You want a lightweight inline editing experience. Collections don't require creating a separate model — the sub-fields are defined directly within the parent model. - The rendering is straightforward. All items render the same way because they all have the same structure. **Use a block (via dynamic block fields) when:** - The repeating structure is used across multiple models. A CTA section that appears on landing pages, blog posts, and product pages should be a block — define it once, reuse it everywhere. - Editors need to mix different item types in the same list. A dynamic block field lets editors choose from hero blocks, feature blocks, testimonial blocks, and CTA blocks in any combination. A collection only supports one item structure. - Each item type has its own visual template. Blocks get their own EJS templates in the site builder, making rendering modular. Collection items are all rendered by the same template logic. - You want structural changes to propagate. Modifying a block model updates every instance across the site. Modifying a collection's sub-fields only affects the model where it's defined. | Capability | Collection | Block (Dynamic) | |---|---|---| | Defined in | Inline within parent model | Separate block model | | Reusable across models | No | Yes | | Multiple item types in same list | No | Yes | | Own template in site builder | No | Yes | | Structural changes propagate | No (local to model) | Yes (site-wide) | | Editor complexity | Lower | Higher | | Best for | Simple repeating lists | Composable, mixed-type sections | The distinction often comes down to a practical question: will this exact repeating structure appear in other models? If yes, use a block. If the list is specific to this model and all items look the same, a collection keeps things simple. --- ## When to Use Collection vs Group Collections and groups both nest sub-fields, but they differ in cardinality. A group produces a single object. A collection produces an array of objects. **Use a group when** the data appears exactly once per record. A page has one hero section, one set of SEO fields, one author attribution. There's no "add another" action — the structure is fixed and singular. **Use a collection when** editors need a variable number of items. An FAQ page needs multiple question-answer pairs. A footer needs multiple social links. The number of items varies per record and editors control how many exist. If you're unsure, ask: "Can there be more than one of these?" If yes, it's a collection. If no, it's a group. → [Group Fields](/model/field/group) --- ## What's Next - [Group Fields](/model/field/group) — Non-repeatable field containers for organizing related fields. - [Block Models](/model/blocks) — Reusable, composable content components for dynamic page sections. - [Dynamic Blocks](/model/fields/dynamic) — Configuring composable layouts with multiple block types. - [Content Field Types](/model/fields) — The full set of field types available in page, entry, and block models. - [Page Models](/model/pages) — How fields combine to define routable page content. - [Entry Models](/model/entries) — How fields combine to define reusable structured data. Block fields embed block model instances into pages and entries. There are two block field types in SleekCMS — a single block field and a dynamic block field — and they serve different purposes. A single block field embeds exactly one block instance of a specific block model. A dynamic block field lets editors compose a variable-length stack of block instances from a set of allowed block types. This page covers both field types: how they work, how they're configured, how they render in templates and the content API, and how to choose between them. The dynamic block field is the more powerful of the two and has its own dedicated page with deeper coverage of the editor experience, allowed block configuration, and layout governance. --- ## Single Block Fields A single block field embeds one instance of a specific block model into a page, entry, or another block. Unlike a dynamic block field where editors choose from multiple block types, a single block field is tied to one block model. The block always appears in the editor, always renders in the output, and always has the same structure. ### When to Use a Single Block Field Use a single block field when a section of the content should always be a specific block type — no choice, no variation. The block is a fixed part of the model's structure, not something editors opt into. **Fixed hero section** — A landing page model that always opens with a hero block. Every page of this type has a hero, and the hero always has the same field structure (heading, subheading, background image, CTA). The hero isn't optional, and editors don't choose it from a list — it's built into the model. **Metadata block** — An entry model that always includes a structured metadata block with fields for author, publish date, reading time, and tags. The metadata block is a permanent part of the entry, not a composable section. **Footer CTA** — A blog post page model that always ends with a CTA block. Every post has the same CTA structure at the bottom. Editors fill in the content, but they can't remove the CTA or swap it for a different block type. The common thread is that the block is mandatory and its type is predetermined. Editors control the content within the block, but not whether the block exists or what type it is. ### Configuring a Single Block Field When you add a single block field to a model, you select which block model it references. That's the entire configuration — the field is now bound to that block model. In the editor, the block's fields appear inline within the parent model, and editors fill them in like any other fields. A single block field produces one object in the API response, nested under the field's handle. Unlike dynamic block fields, there's no `_type` identifier because the block type is always the same — it's defined by the field configuration, not selected at runtime. ### Data Structure A page model with a `hero` single block field that references a "Hero" block model with heading, subheading, and image fields produces this API shape: ```json { "title": "Product Launch", "hero": { "heading": "Introducing Our New Platform", "subheading": "Everything you need, nothing you don't.", "image": { "url": "https://img.sleekcms.com/..." } } } ``` The `hero` key contains the block's field data as a flat object. Your frontend accesses it directly: `page.hero.heading`, `page.hero.image.url`. ### Rendering in Templates In the site builder, a single block field is rendered using the same `render()` helper that handles dynamic blocks. The difference is that you pass a single block object instead of an array: ```ejs <%- render(item.hero) %> ``` This looks up the block model's EJS template, passes the block's field data as `item`, and outputs the rendered HTML. The page template doesn't need to know the block's internal structure — it delegates rendering to the block template. You can also access the block's fields directly in the page template without using `render()`, if you prefer to handle the rendering inline: ```ejs

<%= item.hero.heading %>

<%= item.hero.subheading %>

``` The choice depends on whether the block has its own template you want to reuse. If the same block model appears in multiple places — as a single block field on one model and inside a dynamic block field on another — using `render()` ensures consistent output from the same template. --- ## Dynamic Block Fields A dynamic block field is the composable layout mechanism in SleekCMS. It lets editors build a section of content by adding, removing, reordering, and hiding block instances from a set of allowed block types. The result is an ordered array of block instances — a stack of sections whose composition varies per record. Dynamic block fields are what power the page-builder experience. Editors assemble pages from pre-built sections, each with its own fields and template, while the block models ensure every section is structurally sound. ### Configuring Allowed Block Types When you add a dynamic block field to a model, you configure which block types editors can use. There are three selection modes: **All blocks** — Every block model in the site is available. Use this for maximum flexibility, typically on general-purpose landing page models. New block models are automatically included. **Blocks with prefix** — Only block models whose handle starts with a specific prefix are available. This is a convention-based approach that scales well. A dynamic block field configured with the prefix `blog-` automatically includes any block model whose handle starts with `blog-` (e.g., `blog-hero`, `blog-callout`, `blog-code-sample`). Adding a new block with the matching prefix makes it available without reconfiguring the field. **Selected blocks** — You explicitly choose which block models are available. This is the tightest control and the most common configuration. A homepage model might allow `hero`, `features`, `testimonials`, `pricing`, `cta`, and `faq` blocks. Each model gets exactly the blocks that make sense for its content type. ### Data Structure A dynamic block field produces an array of objects in the API response. Each object includes a `_type` identifier corresponding to the block model handle, plus the block's field data: ```json { "title": "Homepage", "sections": [ { "_type": "hero", "heading": "Welcome", "subheading": "Build something great.", "image": { "url": "https://img.sleekcms.com/..." } }, { "_type": "features", "heading": "Why Us", "items": [...] }, { "_type": "cta", "heading": "Get Started", "button_label": "Sign Up", "button_url": "/signup" } ] } ``` The array order matches the order editors set in the editor. Hidden blocks are excluded from the API response — only visible sections are delivered. ### Rendering in Templates In the site builder, the page template renders all dynamic block sections with a single call to `render()`: ```ejs <%- render(item.sections) %> ``` This iterates over every block instance in the array, looks up each block model's EJS template, renders it with the block's field data as `item`, and concatenates the HTML output. The page template doesn't need to know what types of blocks are present or how to render them — it delegates entirely to the block templates. A separator can be inserted between rendered blocks: ```ejs <%- render(item.sections, '
') %> ``` ### Rendering in Frontend Frameworks When consuming dynamic blocks through the content API, your frontend iterates over the sections array and renders the appropriate component for each block type: ```tsx const blockComponents = { hero: HeroSection, features: FeaturesGrid, testimonials: TestimonialCarousel, cta: CTABanner, }; function PageSections({ sections }) { return ( <> {sections.map(block => { const Component = blockComponents[block._type]; return Component ? : null; })} ); } ``` → [Dynamic Blocks](/model/fields/dynamic) — Full coverage of the editor experience, layout governance, and design system alignment. --- ## Nested Blocks Both single block fields and dynamic block fields support nesting. A block model can include a block field that references other block types, creating hierarchical content structures where sections contain sub-sections. A "Section" block model might include a dynamic block field that allows "Card" blocks, creating a grid of cards within a section. A "Tabs" block model might include a dynamic block field that allows "Tab Panel" blocks. A "Two Column Layout" block might include two single block fields, one for each column. ``` Page └── Dynamic Block Field (sections) ├── Hero Block │ └── Fields (heading, image, CTA) ├── Features Section Block │ └── Dynamic Block Field → Card Blocks │ ├── Card (icon, title, description) │ ├── Card (icon, title, description) │ └── Card (icon, title, description) └── Two Column Block ├── Single Block Field (left) → Content Block └── Single Block Field (right) → Sidebar Block ``` The nesting pattern differs between the two field types. A single block field always embeds one specific block type at a fixed position in the parent block. A dynamic block field lets editors compose the nested content from allowed block types. The choice depends on whether the nested structure is fixed or composable. ### Nesting in the API Nested blocks appear as nested objects and arrays in the API response, following the same structure recursively: ```json { "_type": "features_section", "heading": "Why Choose Us", "cards": [ { "_type": "card", "icon": "⚡", "title": "Fast", "description": "..." }, { "_type": "card", "icon": "🔒", "title": "Secure", "description": "..." } ] } ``` ### Nesting in Templates Nested blocks render through the same `render()` mechanism. A parent block template calls `render()` on its nested block fields, and each nested block renders using its own template: ```ejs

<%= item.heading %>

<%- render(item.cards) %>
``` ```ejs
<%= item.icon %>

<%= item.title %>

<%= item.description %>

``` Keep nesting depth manageable. Two levels — a section containing cards — is intuitive for editors. Three or four levels can make the editing interface difficult to navigate. Design your block architecture to balance structural richness with editorial usability. --- ## Block Reusability Across Models One of the key advantages of both block field types is that the block models they reference are defined independently and can be used across your entire site. A "CTA" block model with heading, button label, and button URL fields can appear as a single block field on a blog post model, inside a dynamic block field on a homepage model, and inside another dynamic block field on a landing page model — all referencing the same block model. This means: **One template, many contexts.** The CTA block's EJS template renders it the same way regardless of which page or entry contains it. Changing the CTA template updates the rendering everywhere. **One schema, many instances.** The CTA block's field structure — its heading, button label, button URL — is defined once. Every model that includes it gets the same editor fields. **Structural changes propagate.** Adding a "background color" field to the CTA block model makes that field available on every page and entry that includes a CTA block, whether through a single block field or a dynamic block field. This is a design system principle applied to content. Block models are your content components, and changes to the component definition propagate across the site. You maintain content structures in one place rather than updating the same fields on every model that uses them. --- ## Choosing Between Single Block and Dynamic Block Fields The two block field types serve different needs, and many models use both. **Use a single block field when:** - The block is mandatory — every record of this model should have it. - The block type is predetermined — editors don't choose between options. - The block appears at a fixed position in the model's structure — it's not part of a composable area. - You want a simpler editing experience — the block's fields appear inline without the overhead of an "add section" interface. **Use a dynamic block field when:** - Editors need to compose layouts from multiple block types. - The number and order of sections varies per record. - Editors should be able to add, remove, reorder, and hide sections. - You want the page-builder experience with live preview. **Combining both** is common. A blog post model might have a single block field for a fixed hero section at the top, fixed fields for title and metadata, and a dynamic block field for the composable article body below. The hero is always there, always the same type. The body sections are composable. | Capability | Single Block Field | Dynamic Block Field | |---|---|---| | Number of block instances | Always one | Variable (editor-controlled) | | Block type | Fixed (one model) | Chosen from allowed set | | Editors can add/remove | No | Yes | | Editors can reorder | No | Yes | | Editors can hide | No | Yes | | API response shape | Object | Array of objects with `_type` | | `render()` input | Single object | Array | | Best for | Fixed, mandatory sections | Composable, flexible layouts | --- ## What's Next - [Dynamic Blocks](/model/fields/dynamic) — Deep dive into the editor experience, allowed block configuration, and layout governance. - [Block Models](/model/blocks) — Defining the block types that block fields reference. - [Content Field Types](/model/fields) — The full set of field types available in page, entry, and block models. - [Group Fields](/model/field/group) — Non-repeatable field containers for organizing related fields. - [Collection Fields](/model/field/collection) — Repeatable inline structures for variable-length lists. - [Page Models](/model/pages) — How block fields fit into page model design. - [Template Context and Data Access](/builder/code/context) — The `render()` helper and other template utilities. Dynamic blocks are how editors compose page layouts from pre-built sections. A dynamic block field turns a page into an ordered stack of content sections — a hero, a features grid, a testimonial carousel, a call-to-action banner — where editors control which sections appear, in what order, and with what content. Each section is a block instance with its own fields and its own view template. This is the mechanism that lets content teams build complete pages visually, without developer involvement for each new page. Developers define the block models and their templates. Editors assemble pages from those blocks, with a live preview alongside the editor showing exactly how the page will render. This page covers how dynamic blocks work, how to configure allowed block types, the editor experience, how pages are rendered as section stacks in the site builder, and how dynamic block data flows through the content API. --- ## What Are Dynamic Blocks A dynamic block field is a special field type you add to a page model (or entry model) that lets editors compose content from a set of allowed block types. Unlike fixed fields where every page of a model has the same structure, a dynamic block field lets each page instance have a different composition of sections. When an editor creates a page with a dynamic block field, they see an interface where they can add block sections from the allowed set, fill in each block's fields, reorder sections by dragging, hide sections without deleting them, and remove sections entirely. The result is a page whose layout is unique to that instance but whose individual sections are all structurally defined by their block models. This is the core page-building experience in SleekCMS. It combines the governance of structured content modeling — every section has a defined schema with typed fields — with the editorial flexibility of a visual page builder. --- ## Configuring Allowed Block Types When you add a dynamic block field to a model, you configure which block types editors can use. This is your primary governance control — editors can only add blocks you've explicitly allowed. There are three selection modes: **All blocks** — Every block model in the site is available. This is the most permissive option and is typically used for general-purpose landing page models where editors need maximum flexibility. As you add new block models to the site, they automatically become available in this field. **Blocks with prefix** — Only block models whose handle starts with a specific prefix are available. This is a convention-based approach that scales well. If you prefix all blog-related blocks with `blog-` (e.g., `blog-hero`, `blog-callout`, `blog-code-sample`), a dynamic block field configured with the `blog-` prefix automatically includes any new block you create with that prefix. You don't need to reconfigure the field when you add blocks — just follow the naming convention. **Selected blocks** — You explicitly choose which block models are available. This gives you the tightest control and is the most common configuration. A homepage model might allow `hero`, `features`, `testimonials`, `pricing`, `cta`, and `faq` blocks. A blog post model might allow `callout`, `code-sample`, `image-gallery`, and `cta` blocks. Each model gets exactly the blocks that make sense for its content type. The selection mode is a modeling decision you make once per dynamic block field. You can change it later — switching from selected blocks to prefix-based, or adding new blocks to the selected list — without affecting existing content. --- ## Pages as Stacks of Sections With dynamic blocks, a page is conceptually a stack of sections. Each section is a block instance with its own content, and the page's layout is the sequence of those sections from top to bottom. This model maps directly to how modern marketing pages, landing pages, and content-rich sites are built. A typical page might look like: ``` Page: Homepage ├── Hero Block → Full-width hero with heading, subheading, CTA ├── Logos Block → Client logo strip ├── Features Block → Three-column feature grid ├── Testimonials Block → Carousel of customer quotes ├── Pricing Block → Pricing tier comparison ├── FAQ Block → Accordion of common questions └── CTA Block → Final call-to-action banner ``` Each of these sections is an independent block instance. The hero has its own fields (heading, subheading, background image, CTA button). The pricing block has its own fields (tiers, prices, feature lists). They don't share data — they're self-contained sections that together form the page. An editor building this page adds each block type in order, fills in the content for each section, and sees the result immediately in the preview panel. A different landing page might use only three of these blocks, or arrange them in a different order, or include two CTA blocks in different positions. Every page instance can have its own composition. --- ## The Editor Experience The dynamic block editor is designed for visual page composition. Editors work in a split-panel interface with the content editor on one side and a live preview on the other. ### Adding Sections Editors add a new section by selecting a block type from the allowed set. The block's fields appear in the editor, and the corresponding section renders immediately in the preview. Editors fill in the content — heading text, images, button labels — and see the rendered section update in real time. ### Reordering Sections Sections can be reordered by dragging them to a new position. Moving the testimonials block above the pricing block is a drag operation, and the preview updates to reflect the new order. This makes it easy to experiment with page flow without re-entering content. ### Hiding Sections Editors can hide a section without deleting it. A hidden section retains all its content but doesn't render in the preview or on the published page. This is useful for seasonal content — a holiday promotion banner that gets hidden after the campaign ends and can be unhidden next year with all its content intact. It's also useful during drafting, when editors want to prepare sections in advance and reveal them later. ### Removing Sections Editors can remove a section entirely, which deletes it and its content from the page. This is a permanent action — unlike hiding, removing a section means its content is gone. ### Live Preview The preview panel shows the page as it will appear on the published site. Because each block model has its own view template in the site builder, the preview renders the actual HTML output — not a wireframe or approximation. Editors see real typography, real layout, real images. This side-by-side workflow means editors don't need to publish or preview in a separate tab to see how their changes look. --- ## Rendering Dynamic Blocks in Templates In the site builder, dynamic blocks are rendered using the `render()` helper function. When a page model has a dynamic block field — typically called `sections` — the page template delegates rendering to each block's own template with a single call. ### The `render()` Function The page template renders all dynamic block sections with: ```ejs <%- render(item.sections) %> ``` This single line iterates over every block instance in the `sections` field, looks up the corresponding block model's EJS template, renders it with the block's field data as `item`, and concatenates the HTML output. The page template doesn't need to know what types of blocks are present or how to render them — it delegates entirely to the block templates. ### How It Works End to End The full rendering pipeline for a page with dynamic blocks follows this flow: **1. Layout template** — The shared layout wraps the page with the outer HTML shell: ``, ``, navigation, footer. **2. Page template** — The page template renders the page's fixed fields (title, metadata) and calls `render(item.sections)` to output the dynamic block area. **3. Block templates** — Each block in the `sections` array is rendered by its own template. A hero block uses the hero template, a pricing block uses the pricing template. Each block template receives its own field data as `item`. ``` Layout Template └── Page Template ├── Fixed fields (title, meta) └── render(item.sections) ├── hero.ejs → item = { heading, subheading, image, cta } ├── features.ejs → item = { items, layout, heading } ├── testimonials.ejs → item = { quotes, style } └── cta.ejs → item = { heading, buttonLabel, buttonUrl } ``` ### A Practical Example A simple page template for a landing page model: ```ejs

<%= item.title %>

<%- render(item.sections) %>
``` A hero block template that renders one of the sections: ```ejs

<%= item.heading %>

<%= item.subheading %>

<%= item.cta_label %>
``` A CTA block template: ```ejs

<%= item.heading %>

<%= item.button_label %>
``` The page template stays minimal. All the section-level rendering logic lives in the block templates, where it's modular and reusable. The same hero block template renders the hero section on the homepage, on a product landing page, and on a campaign page — wherever that block type is used. → [Site Builder](/builder) → [Model Templates](/builder/code/main) → [Template Context and Data Access](/builder/code/context) --- ## Layout Flexibility vs Governance Dynamic blocks create a spectrum between fully flexible and fully governed page layouts. Where you land on that spectrum is a modeling decision. **Maximum flexibility** — A dynamic block field set to "all blocks" with a large block library lets editors build almost any page layout. This works well for marketing teams that need to create diverse landing pages quickly, but it relies on editors making good composition choices. **Moderate governance** — A dynamic block field with a curated set of 5–10 selected blocks gives editors meaningful layout options while ensuring every section on the page is one you've designed and templated. This is the most common setup — enough flexibility for varied pages, enough structure to maintain design consistency. **Tight governance** — A dynamic block field with only 2–3 allowed blocks, or a page model with mostly fixed fields and a small dynamic area, gives editors limited composition control. This works for content types where the layout should be mostly consistent across instances — blog posts where only the body section is composable, or product pages with a fixed structure and one flexible area. You can also combine fixed fields and dynamic blocks on the same page model. A blog post model might have fixed fields for title, author, publish date, and featured image, plus a dynamic block field for the article body. The header structure is consistent across all posts, while the body content is composable. --- ## Design System Alignment Dynamic blocks naturally align with design system thinking. Each block model is a component with defined fields, and each block template is the component's rendering implementation. The block library becomes your site's component library, and the allowed blocks on each page model are the components available in that context. This means your design system and your content model stay in sync. When a designer adds a new section type to the design system, you create a corresponding block model with the right fields and a template that implements the design. The block immediately becomes available to editors on any page model that allows it. Changes to a component's design — updating the testimonial card layout, adjusting the CTA banner's spacing — are made in the block template. The change takes effect everywhere that block is used, across all pages. Editors don't need to do anything; the content stays the same and the rendering updates. --- ## When to Use Dynamic Blocks Dynamic blocks are the right choice when page layouts vary across instances and editors need to control composition. They're not always necessary. **Use dynamic blocks when:** - Different pages of the same type need different section layouts. Landing pages, marketing campaigns, and long-form content pages are classic cases. - Editors need to add, remove, or reorder sections without developer help. The visual page builder workflow depends on dynamic blocks. - You want a component-based content architecture where sections are reusable across page types. - The page building experience — editor plus live preview — is important for your content team. **Use fixed fields instead when:** - Every page of the same type has an identical structure. A simple "About" page with a fixed set of fields doesn't benefit from dynamic blocks. - The content is short and predictable. A contact page with an address, phone number, and map doesn't need composable sections. - Editors shouldn't have layout control. Some page types should look the same across all instances, and fixed fields enforce that. Many page models use both. Fixed fields at the top for metadata, title, and hero content, with a dynamic block field for the composable body area below. This gives you a consistent page header with a flexible content area. --- ## Dynamic Blocks in the Content API When consuming dynamic blocks through the API, the field returns an array of block objects. Each object includes a `_type` identifier corresponding to the block model handle and the block's field data. ```typescript const page = client.getPage('/'); // page.sections → [ // { _type: 'hero', heading: 'Welcome', subheading: '...', image: { url: '...' } }, // { _type: 'features', heading: 'Why Us', items: [...] }, // { _type: 'cta', heading: 'Get Started', button_label: 'Sign Up', button_url: '/signup' } // ] ``` Your frontend iterates over the sections array and renders the appropriate component for each block type. Hidden blocks are excluded from the API response — only visible sections are delivered. The order of the array matches the order editors set in the editor. If an editor moves the CTA block above the features block, the API response reflects that order. Your frontend renders the array in sequence to produce the same layout the editor composed. → [Content API](/publish/api) → [@sleekcms/client](/publish/api/client) --- ## What's Next - [Block Models](/model/blocks) — Defining the block types that editors compose pages from. - [Block Fields](/model/fields/block) — Using a single block field vs a dynamic block field. - [Content Field Types](/model/fields) — The full set of field types available in models. - [Page Models](/model/pages) — How dynamic block fields fit into page model design. - [Site Builder](/builder) — The integrated coding environment where block templates are built. - [Model Templates](/builder/code/main) — Binding EJS templates to block, page, and entry models. - [Content Editing](/content) — The full editing experience for content creators. The content editor is where you create and update the pages, entries, and blocks that make up your site. It's a form-based interface generated from your content models — every field you defined in a model becomes an input in the editor. You fill in the fields, save, and your content is ready for the API or the site builder. This page covers the editing workflow for all content types, how dynamic blocks and collections work in the editor, draft and publish states, AI-assisted writing, live preview with the site builder, language and environment tools, and access control for admin-only content. --- ## The Editor Interface When you open a content record — a page or an entry — the editor displays the fields defined by that record's model. Text fields become text inputs, image fields become image pickers, dropdowns show their option set values, and structural fields like groups and collections render as nested sections. Blocks are edited inline within the page or entry that contains them — you don't open a block as a separate record. The editor layout follows the model structure. Fields appear in the order they were defined. Groups render as collapsible sections that organize related fields. The interface is the same regardless of whether you're editing a static page, a page collection record, an entry, or an admin-only entry — the fields change, but the workflow doesn't. ### Saving Content After making changes, save the record to persist your updates. Saving writes the content to the current environment. If you're consuming content through the API, the updated record is available immediately in API responses (subject to caching and environment configuration). If you're using the site builder, saving the content updates the data but does not automatically update the rendered page. You need to rebuild the page after saving to see your changes in the preview. This is because the site builder generates static HTML — the content and the rendered output are separate artifacts, and a build step connects them. --- ## Editing Dynamic Blocks Pages and entries that include a dynamic block field present blocks as a composable stack in the editor. When you first open a record, existing blocks appear in their collapsed state, each showing a summary of its content so you can scan the page structure at a glance without expanding every section. ### Adding, Removing, and Reordering Blocks To modify the block composition, click **Edit Blocks**. This opens the block management interface where you can add new block instances from the allowed block types, remove blocks you no longer need, and reorder blocks by dragging them into a new position. The allowed block types are determined by the model — you can only add blocks that the model's dynamic block field permits. ### Block Titles Each block instance can be given a title for reference. This title is a UI-only label — it doesn't appear on the published page or in the API response. Its purpose is to help you and other editors identify what a block is about when scanning the collapsed block list. A hero block might be titled "Homepage Hero — Spring Campaign", a CTA block might be titled "Free Trial CTA". These labels make it easier to navigate pages with many sections. ### Hiding Blocks You can hide a block without deleting it. A hidden block retains all its content but doesn't render on the published page and is excluded from API responses. This is useful for seasonal content, A/B testing preparation, or any case where you want to temporarily remove a section and bring it back later with its content intact. → [Dynamic Blocks](/model/fields/dynamic) → [Block Models](/model/blocks) --- ## Editing Collections Collection fields — repeatable groups of fields within a model — render as a list of items in the editor. Each item contains the same set of sub-fields defined by the collection. You can add new items, remove existing ones, and reorder items within the list. Collections appear in both page models and entry models. An FAQ collection on a page might contain question-and-answer pairs. A features collection on a block might contain icon, title, and description sets. The editing experience is the same: each item is a mini-form with the collection's sub-fields, and you manage the list of items directly in the editor. → [Collection Fields](/model/field/collection) --- ## Draft and Published States Records in page collections and entry collections can be marked as draft. A draft record is saved in the editor and visible to content teams, but it is not included in content API responses. This means your frontend application or site builder output won't include draft content — only published records are delivered. Drafts are useful for content that is in progress. You can create a blog post, fill in the fields over multiple sessions, and keep it in draft state until it's ready. When you're satisfied with the content, remove the draft flag and save to publish it. If you need to pull a published record back for revisions, mark it as draft again — it disappears from the API until you republish. Draft state is available on collection records (page collections and entry collections). Static pages and single entries don't have a draft state — they're always published when saved, since there's only one instance and it's expected to be live. → [Draft Entries](/content/draft) --- ## AI Writing Assistance SleekCMS integrates AI tools directly into the content editor to help with writing and content generation. These tools are available on markdown and rich text fields, and as a record-level feature for populating new records. ### Revise with AI When editing a markdown or rich text field, you can use the **Revise with AI** option to clean up existing content. The AI fixes typos, corrects minor grammar issues, and improves readability without changing the meaning or substance of your writing. It preserves your voice and intent — this is a polish pass, not a rewrite. This is useful for quick proofreading before publishing. Write your content naturally, then run a revision pass to catch the small errors that are easy to miss on your own. ### Draft with AI For markdown and rich text fields, the **Draft with AI** option generates new content from a prompt you provide. You describe what you want — the topic, desired length, writing style — and the AI produces a draft that you can then edit and refine. For example, you might prompt it to write a 300-word introduction for a blog post about sustainable packaging, in a conversational tone. The generated content is a starting point. You review it, adjust the language, add specific details, and make it your own. The AI handles the blank-page problem — getting words down that you can shape into the final piece. ### Draft a New Record with AI When creating a new record, you can use AI to populate the empty fields in the form. Any fields you've already filled in are used as context — if you've set the title and selected a category, the AI uses that information to generate appropriate content for the remaining fields like body text, meta descriptions, or excerpt fields. This accelerates content creation for records that follow predictable patterns. Blog posts, product descriptions, landing page sections — any content type where the structure is defined and the AI can infer reasonable content from a title and a few contextual fields. --- ## Live Preview with the Site Builder If you're using the integrated site builder, the editor supports a side-by-side preview of the rendered page. As you edit content in the form, the preview panel shows how the page will look when built — real HTML rendered from your EJS templates with the current content data. The preview reflects the saved state of the content. After making changes and saving, you need to rebuild the page to see the updated preview. The rebuild is fast for single pages — the builder compiles just that page, not the entire site — so the feedback loop between editing and previewing is near-instant. This workflow lets you iterate on content with visual feedback. Edit a heading, save, rebuild, and see the result. Adjust an image, save, rebuild, and check the layout. The editor and preview sit side by side, so you don't need to switch tabs or navigate to a separate preview URL. → [Site Builder](/builder) → [Preview and Iteration](/builder#preview-and-iteration) --- ## Language and Environment Tools The editor includes tools for working with multilingual content and comparing content across environments. ### Language Selection If your site supports multiple languages, you can select the language you want to edit from the editor interface. Switching languages loads the content for that language version of the record, letting you create and update translations within the same editing workflow. ### Content Comparison You can compare the current record's content with another language version or with content from a different environment. This is useful when translating content — you can view the original language side by side with the translation you're writing. It's also useful for reviewing differences between environments, such as comparing staging content against what's live in production. → [Content Translations](/content/translate) → [Compare Content](/content/compare) → [Environments & Content Versions](/publish/envs) --- ## Access Control for Admin-Only Content Entry models can be configured as admin-only, which restricts who can edit them. Admin-only entries — typically used for site-wide settings like navigation, footer content, and global configuration — are only editable by users with the admin role. Content editors can view and use the data from admin-only entries (they appear in templates and API responses), but they cannot modify the records. This separation protects site-critical configuration from accidental changes while keeping the content accessible for rendering. If you don't have admin access and need to update an admin-only entry, contact a site administrator. The admin-only restriction applies to editing through the SleekCMS interface — it doesn't affect how the content is consumed through the API or site builder. → [Admin Role](/settings/members/admin) → [Entry Models](/model/entries) --- ## What's Next - [Draft Entries](/content/draft) — Managing draft and published states for collection records. - [Content Translations](/content/translate) — Creating and managing content in multiple languages. - [Compare Content](/content/compare) — Side-by-side comparison across languages and environments. - [Content Modeling](/model) — How models define the editing experience. - [Dynamic Blocks](/model/fields/dynamic) — Composing pages from block sections. - [Site Builder](/builder) — The integrated build and preview system. - [Content API](/publish/api) — How edited content flows to your frontend. SleekCMS provides a centralized media system for managing images, videos, and documents across your site. Instead of scattering media files across individual content records, all media lives in one place — accessible to every model, every page, and every team member. This page covers how media management works in SleekCMS, what media types are supported, and how centralized media improves collaboration and maintainability. Implementation details for image optimization, video embedding, and file handling are covered in the linked sub-pages. --- ## Why Centralized Media In a traditional CMS workflow, media files are often uploaded inline — attached to a specific blog post, embedded in a particular page, managed by whoever happens to be editing that content. This creates problems as your site grows. The same logo gets uploaded four times. A product photo lives in one post but can't be found when another team member needs it. Updating a headshot means hunting through every page that uses it. SleekCMS takes a different approach. All media — images, videos, and documents — is managed in a shared media library at the site level. Content records reference media from this library rather than owning their own copies. This has several practical benefits. **Team collaboration.** Any team member with media access can upload, organize, and manage assets independently of content editing. A designer can prepare images while a writer drafts copy. A marketing manager can update brand assets without touching page content. Media management and content editing are parallel workflows, not sequential ones. **Update once, update everywhere.** When a media asset is referenced from the library rather than duplicated per record, replacing that asset updates it everywhere it's used. Swap out a product photo in the media library and every page, entry, and block that references it reflects the change immediately — no need to edit individual content records or update code. **Consistency across the site.** A shared library prevents duplicate uploads and naming inconsistencies. Your team works from a single source of truth for visual assets, which keeps the site's media organized and reduces storage waste. --- ## Media Types SleekCMS supports three categories of media, each with different capabilities and workflows. ### Images Images are the most fully featured media type in SleekCMS. The platform handles the entire image lifecycle — upload, storage, transformation, optimization, and delivery — with no external services or build configuration required. When you upload an image, SleekCMS stores the original file and makes it available through a CDN-backed URL. From there, you can transform and optimize images on the fly using URL parameters — resize, crop, change format, adjust quality, apply filters — without touching the original file or writing any code. Image fields in your content models reference images from the media library. Editors select images through a picker that supports multiple sources: the site's media gallery for previously uploaded images, local file upload, and integrated third-party sources including Unsplash, Pexels, Pixabay, and Iconify. Editors can also provide a direct URL for externally hosted images. In templates, the site builder provides helper functions — `src()`, `img()`, `picture()`, and `svg()` — that generate optimized image URLs and HTML elements with the correct attributes. These helpers handle responsive images, dark/light theme variants, format conversion, and fallback behavior automatically. → [Image Management & Optimization](/media/images) ### Videos Video fields store references to externally hosted video content — typically YouTube, Vimeo, or other video platform URLs. SleekCMS does not currently host video files directly. You add a video field to your content model, and editors provide the URL to a hosted video. Your templates or frontend code handles the embed rendering. This approach keeps your CMS lightweight and leverages the streaming infrastructure of dedicated video platforms. Native video hosting is planned for a future release. ### Documents Document assets — PDFs, spreadsheets, presentations, and other files — can be uploaded and managed through the media library. These are available for download linking in your content or for direct reference from content fields. Documents are stored and served as-is, without the transformation capabilities available for images. They're useful for downloadable resources like whitepapers, menus, brochures, spec sheets, and any file your site visitors need to access. → [File and Document Assets](/media/files) --- ## Image Transformation and Optimization One of the most powerful aspects of SleekCMS's media system is native image transformation. Every image in your media library can be resized, cropped, converted, and optimized on the fly through URL parameters — no image editing software, no build-time processing, no third-party services to configure. The transformation service supports a comprehensive set of operations: **Resizing and cropping** — Set width, height, and fit mode (cover, contain, fill, inside, outside). Specify crop position and gravity for precise control over what part of the image is shown. Support for device pixel ratio means retina-ready images with a single parameter. **Format conversion** — Convert between WebP, AVIF, PNG, JPEG, GIF, and TIFF on the fly. Serve modern formats like WebP and AVIF to browsers that support them, with automatic quality control. **Visual adjustments** — Apply blur, sharpening, rotation, flipping, rounded corners, grayscale, color tinting, and brightness/contrast/saturation adjustments. These are applied at the CDN edge, not stored as separate files. **Padding and borders** — Extend image edges with configurable padding and background colors, useful for creating consistent aspect ratios or adding visual breathing room. All transformations are applied via URL query parameters on the image's CDN URL. The original file is never modified. This means the same source image can be served at different sizes, formats, and crop ratios across your site — a hero banner version, a thumbnail version, and an Open Graph social preview version — all from a single upload. In the site builder, the `src()` helper generates these transformation URLs from simple parameters: ```ejs <%- item.hero.alt %> ``` Through the content API, image objects include the raw URL that you can append parameters to in your own frontend: ```typescript const page = client.getPage('/blog/hello-world'); // page.image.raw → 'https://img.sleekcms.com/az41/m7urgy6z' // Append params: page.image.raw + '?w=800&h=600&fmt=webp' // Or use the ready-made URL with default format: // page.image.url → 'https://img.sleekcms.com/az41/m7urgy6z.webp' ``` → [Image Management & Optimization](/media/images) → [Image Processing Configuration](/settings/config/imgix) --- ## Media in Content Models Media integrates into your content models through dedicated field types. Each field type is designed for a specific media category and provides the appropriate editor experience. **Image fields** support single or multiple image selection. A single image field is used for hero images, author headshots, or logos. A multiple image field is used for photo galleries, product image carousels, or any content that needs more than one image. Editors pick from the media gallery, upload new files, or pull from integrated sources — all within the content editor. **Video fields** store a single external video URL. Editors paste in a YouTube, Vimeo, or other platform link. The URL is stored as a string in the content record. **Reference fields** can link to document entries if you model your documents as an entry collection. This gives you structured metadata alongside the file — a title, description, category, and the file itself. In API responses, image fields return an object with the image URL, alt text, raw URL for transformations, source information, and theme variant URLs for dark/light mode support. Video fields return the URL string. This gives your frontend everything it needs to render media correctly. → [Content Field Types](/model/fields) --- ## Media in Templates The site builder provides a set of helper functions specifically for working with media in your EJS templates. These helpers handle the common patterns — generating optimized URLs, building responsive HTML elements, supporting theme variants — so your templates stay clean. **`src(obj, attr)`** generates an optimized image URL with resizing parameters. Pass an image object and dimensions, get back a CDN URL with the right query parameters. **`img(obj, attr)`** generates a complete `` HTML element with the optimized source URL, alt text, and optional CSS class and style attributes. **`picture(obj, attr)`** generates a `` element with automatic dark/light theme variant support using `prefers-color-scheme` media queries. When an image has theme variants configured, the `picture()` helper includes the appropriate `` elements. **`svg(obj, attr?)`** inlines an SVG file with custom attributes applied directly to the SVG element. This is useful for logos, icons, and other vector graphics where you need to control size, color, or CSS class via attributes. All image helpers include fallback behavior — when an image object is missing or invalid, they generate a placeholder image URL so layouts don't break during development. → [Template Context and Data Access](/builder/code/context) → [Model Templates](/builder/code/main) --- ## Media and the Content API When consuming media through the content API rather than the site builder, media data is included inline in the content records that reference it. An image field returns an object with this structure: ```typescript const page = client.getPage('/blog/hello-world'); // page.image → { // url: 'https://img.sleekcms.com/az41/m7urgy6z.webp', // CDN URL with default format // raw: 'https://img.sleekcms.com/az41/m7urgy6z', // Base URL for custom transformations // alt: 'Description of the image', // Alt text set by editors // source: 'unsplash', // Source platform (if applicable) // light: null, // Light theme variant URL // dark: null // Dark theme variant URL // } ``` Named images — images uploaded to the media library with a specific key rather than attached to a content field — can be retrieved directly: ```typescript const logo = client.getImage('site-logo'); // { url: '...', raw: '...', alt: 'Company logo' } ``` Your frontend uses the `raw` URL to build custom transformation URLs by appending query parameters, or uses the `url` directly for the default format. → [Content API](/publish/api) → [@sleekcms/client](/publish/api/client) --- ## What's Next - [Image Management & Optimization](/media/images) — Uploading, organizing, and transforming images with the built-in optimization service. - [File and Document Assets](/media/files) — Managing PDFs, documents, and downloadable files. - [Content Field Types](/model/fields) — Image, video, and other media field types in your content models. - [Template Context and Data Access](/builder/code/context) — The `src()`, `img()`, `picture()`, and `svg()` helper functions for rendering media in templates. - [Image Processing Configuration](/settings/config/imgix) — Configuring image processing settings for your site. SleekCMS provides a complete image management system — upload, organize, transform, optimize, and deliver images from a single platform. Every image is stored centrally, served from a CDN, and can be transformed on the fly through URL parameters without modifying the original file. This means a single uploaded image can serve as a full-width hero banner, a thumbnail card, a retina-ready social preview, and a blurred background — all without creating separate versions. This page covers how images work in SleekCMS, how to add and manage them, image metadata and handles, the transformation and optimization system, how images appear in templates and API responses, and how to use transformed images in markdown and rich text content. --- ## How Images Work When you add an image to SleekCMS — whether through upload, an integrated source, or a URL — the platform stores the original file and assigns it a CDN-backed URL. This URL is permanent. The image is immediately available across your entire site, in any content model that uses an image field. Every image has two URLs: **`url`** — The CDN URL with a default format applied (typically WebP). This is the ready-to-use URL for direct rendering. **`raw`** — The base CDN URL without format or transformation parameters. This is the URL you append query parameters to for custom transformations. The original file is never modified. All transformations — resizing, cropping, format conversion, filters — are applied at request time via query parameters on the `raw` URL. The CDN caches transformed versions, so subsequent requests for the same transformation are served instantly. --- ## Adding Images Images can be added to SleekCMS through multiple sources, all accessible from the image picker in the content editor. **Upload** — Drag and drop or select files from your local machine. Uploaded images are stored in SleekCMS's media storage and served from the CDN. **Unsplash** — Browse and search the Unsplash library directly from the image picker. Selected images are stored in your media library with their original attribution metadata. **Pexels** — Search the Pexels stock photo library and add images without leaving the editor. **Pixabay** — Access Pixabay's free image library for photos, illustrations, and vector graphics. **Iconify** — Browse icon sets from Iconify's library. Selected icons are stored as SVG images, useful for feature icons, UI elements, and decorative graphics. **URL** — Provide a direct URL to an externally hosted image. SleekCMS references the image at that URL, which is useful for images already hosted elsewhere that you don't want to re-upload. All sources are available from the same image picker interface. Editors choose the source, find or upload the image, and the result is a consistent image object in the content record regardless of where the image came from. --- ## Image Metadata Every image in SleekCMS carries metadata that editors can manage and that your templates and frontend code can consume. ### Alt Text Each image has an **alt** description field. Editors enter a text description of the image, which is used for the `alt` attribute in HTML and for accessibility. Alt text is stored alongside the image and delivered in API responses, so your rendering code always has access to it without maintaining a separate mapping. Alt text is set per image instance — when an editor selects an image for a content field, they can provide or edit the alt description for that usage. This means the same image can have different alt text in different contexts, which is the correct approach since alt text should describe the image's purpose in its specific context. ### Default Dimensions and Filters Image metadata can include **default width, height, and filter settings**. These defaults define how the image should be rendered when no explicit transformation parameters are provided. A logo image might have default dimensions of 200×50 to ensure consistent sizing across the site. A profile photo might have a default filter applied. Default metadata acts as a baseline — your templates and frontend code can override these values with explicit parameters when needed, but the defaults ensure consistent rendering when no overrides are specified. ### Theme Variants Images can have **dark** and **light** theme variants — alternate versions of the same image optimized for different color schemes. A logo with a dark background variant and a light background variant, for example. When theme variants are present, the `picture()` template helper automatically generates `` elements with `prefers-color-scheme` media queries. --- ## Image Handles When you assign a **handle** to an image in the media library, that image becomes directly accessible through the content API and in templates by name, independent of any content field. Handles are useful for site-wide images that aren't tied to a specific content record — your site logo, a default Open Graph image, a favicon, a background pattern. Instead of storing these in a settings entry or hardcoding URLs, you give the image a handle and access it directly. ### In the Content API Images with handles appear in the `images` object of the content payload and can be retrieved with the `getImage()` method: ```typescript const logo = client.getImage('site-logo'); // { // url: 'https://img.sleekcms.com/az41/logo.webp', // raw: 'https://img.sleekcms.com/az41/logo', // alt: 'Company Logo' // } ``` The full content payload includes all handled images under the `images` key: ```typescript const content = client.getContent(); // content.images → { // 'site-logo': { url: '...', raw: '...', alt: '...' }, // 'og-default': { url: '...', raw: '...', alt: '...' }, // 'favicon': { url: '...', raw: '...', alt: '...' } // } ``` ### In Templates In the site builder, handled images are accessible through the `getImage()` function in the template context: ```ejs <% const logo = getImage('site-logo'); %> <%- logo.alt %> ``` This makes handled images available in any template — layout, page, block, or entry — without needing to pass them through content fields or entry references. --- ## Replacing Images Without Changing URLs One of the most important properties of SleekCMS's image system is that images can be replaced without changing their URL. When you upload a new version of an image — an updated logo, a refreshed product photo, a corrected headshot — the URL stays the same. Every page, entry, block, and template that references that image automatically serves the new version. This is possible because image URLs are tied to the image's identity in the media library, not to the file contents. Replacing the file updates what the URL serves without breaking any references. This has practical benefits across the entire workflow. Designers can update brand assets without involving content editors. Product teams can refresh imagery without editing content records. Marketing can swap seasonal photos without touching templates. The URL is the stable reference, and the file behind it can change as needed. --- ## CDN Delivery All images in SleekCMS are served from a CDN. This means images are delivered from edge servers geographically close to your visitors, with automatic caching for fast load times. When a transformation is requested — a specific size, format, or filter — the CDN caches the transformed result. The first request for a particular transformation processes the image on the fly. Subsequent requests for the same transformation are served directly from the CDN cache with no processing delay. This architecture means you don't need to pre-generate image variants or maintain separate files for different sizes. You define the transformation you need at the point of use — in your template, in your frontend code, or in a markdown image URL — and the CDN handles caching and delivery. --- ## Image Transformation Reference Every image in SleekCMS can be transformed on the fly by appending query parameters to the image's `raw` URL. Transformations are processed server-side, cached at the CDN edge, and the original file is never modified. The base URL pattern is: ``` https://img.sleekcms.com/{site-id}/{image-id}?{parameters} ``` ### Dimensions | Parameter | Range | Description | |---|---|---| | `s` | WxH | Combined size (e.g., `300x200`) | | `w` | 1–8192 | Width in pixels | | `h` | 1–8192 | Height in pixels | | `dpr` | 1–4 | Device pixel ratio multiplier | `w` and `h` override values from `s`. Final dimensions are multiplied by `dpr`, so `w=400&dpr=2` produces an 800px wide image suitable for retina displays. ### Resize and Crop | Parameter | Values | Description | |---|---|---| | `fit` | `cover` (default), `contain`, `fill`, `inside`, `outside` | Resize fitting mode | | `pos` | Position keyword or gravity | Position/gravity for cropping | | `bg` | Hex color | Background color (e.g., `fff`, `ff0000`, `ffffff80`) | **Fit modes:** - **`cover`** — Resize to fill the target dimensions, cropping edges as needed. This is the default and the most common choice for thumbnails and cards. - **`contain`** — Resize to fit within the target dimensions, adding background padding if the aspect ratio doesn't match. Use `bg` to set the padding color. - **`fill`** — Stretch the image to exactly match the target dimensions, ignoring aspect ratio. - **`inside`** — Resize to fit within the target dimensions without exceeding either. Similar to `contain` but without padding. - **`outside`** — Resize so the image covers the target dimensions, potentially exceeding one dimension. **Position values for cropping:** `top`, `right top`, `right`, `right bottom`, `bottom`, `left bottom`, `left`, `left top`, `center`, `north`, `northeast`, `east`, `southeast`, `south`, `southwest`, `west`, `northwest`, `entropy`, `attention` The `entropy` and `attention` positions are smart crop modes — `entropy` focuses on the area with the most detail, and `attention` uses saliency detection to focus on the most visually interesting region. ### Format and Quality | Parameter | Values | Description | |---|---|---| | `fmt` | `webp`, `avif`, `png`, `jpg`, `jpeg`, `gif`, `tiff`, `svg` | Output format | | `q` | 1–100 (default: 80) | Output quality | Format conversion happens on the fly. Serve modern formats like WebP or AVIF to reduce file sizes without maintaining separate versions. The `svg` format returns the original file without any raster transformations applied. ### Transformations | Parameter | Range | Description | |---|---|---| | `rot` | `0`, `90`, `180`, `270` | Rotation in degrees | | `flip` | `h`, `v`, `hv` | Flip horizontal, vertical, or both | | `blur` | 0.3–1000 | Gaussian blur sigma | | `sharp` | 0.5–10 | Sharpen sigma | | `round` | 0–9999 | Border radius for rounded corners | | `p` | 0–1000 | Padding (extend image edges) | **Padding formats:** - `p=20` — Uniform padding on all sides - `p=10,20` — Vertical (top/bottom), horizontal (left/right) - `p=10,20,30,40` — Top, right, bottom, left Padding uses the `bg` color if specified, otherwise transparent. ### Color Adjustments | Parameter | Range | Description | |---|---|---| | `gray` | `1`, `true`, or present | Convert to grayscale | | `tint` | Hex color | Apply color tint overlay | | `bri` | -100 to 100 | Brightness adjustment | | `con` | -100 to 100 | Contrast adjustment | | `sat` | -100 to 100 | Saturation adjustment | ### Filters | Parameter | Values | Description | |---|---|---| | `filter` | Comma-separated list | Apply multiple filters | Available filters: `grayscale`, `greyscale`, `negate`, `normalize`, `clahe` Multiple filters can be combined: `filter=normalize,negate` ### Transformation Examples ``` # Resize to 400×300, cover fit, WebP format ?w=400&h=300&fmt=webp # Combined size with quality control ?s=800x600&q=90 # Thumbnail with rounded corners ?w=150&h=150&round=75&fmt=png # Retina display (2x device pixel ratio) ?w=400&h=300&dpr=2 # Grayscale with blur (useful for backgrounds) ?w=800&gray=1&blur=2 # Crop focusing on bottom-right with background fill ?w=500&h=500&fit=contain&pos=southeast&bg=ffffff # Color adjustments ?w=600&bri=10&con=20&sat=-50 # Multiple filters ?w=800&filter=normalize,negate # Rotate and flip ?w=600&rot=90&flip=h # Add padding with background color ?w=400&p=20&bg=f0f0f0 # Asymmetric padding (10px vertical, 30px horizontal) ?w=400&p=10,30&bg=ffffff ``` --- ## Images in Templates The site builder provides four helper functions for working with images in EJS templates. These helpers generate optimized URLs and HTML elements from image objects, handling transformation parameters, alt text, theme variants, and fallback behavior. ### `src(obj, attr)` Generates an optimized image URL with transformation parameters. Returns a URL string. ```ejs <%- item.hero.alt %> ``` When the image object is invalid or missing, `src()` returns a placeholder URL (`https://placehold.co/{size}.png`) so layouts don't break during development. ### `img(obj, attr)` Generates a complete `` HTML element with the optimized source, alt text, and optional CSS attributes. This is a simpler alternative to `picture()` when theme variants are not needed. ```ejs <%- img(item.hero, '800x600') %> <%- img(item.hero, { size: '800x600', class: 'hero-img', style: 'object-fit: cover' }) %> ``` ### `picture(obj, attr)` Generates a `` element with automatic dark/light theme variant support. When the image has `dark` or `light` variants, the helper includes `` elements with `prefers-color-scheme` media queries. ```ejs <%- picture(item.hero, '800x600') %> <%- picture(item.logo, { size: '200x50', class: 'logo', style: 'max-width: 100%' }) %> ``` ### `svg(obj, attr?)` Inlines an SVG file with custom attributes applied directly to the SVG element. Use this for logos, icons, and vector graphics where you need to control size, color, or CSS class through attributes. ```ejs <%- svg(item.icon) %> <%- svg(item.logo, { width: 120, height: 40, class: 'logo' }) %> ``` Returns an empty SVG with an error attribute if the image object is invalid or the URL doesn't end with `.svg`. --- ## Using Transformed Images in Markdown When you include images in markdown or rich text fields, you can apply transformations directly in the image URL. This is useful for controlling how inline images render in article body content, blog posts, or any content authored in markdown. Since markdown images use standard URL syntax, you append transformation parameters to the image's `raw` URL just as you would anywhere else: ```markdown ![A plate of pasta](https://img.sleekcms.com/az41/m7urgy6z?w=800&h=400&fit=cover&fmt=webp) ![Author headshot](https://img.sleekcms.com/az41/m7uxv6ze?w=150&h=150&round=75&fmt=webp) ![Background](https://img.sleekcms.com/az41/m7utzbuj?w=1200&gray=1&blur=3&q=60) ``` This gives content authors precise control over image rendering within their written content. The same transformation parameters available in templates and API integrations work identically in markdown image URLs — resize, crop, format convert, adjust quality, apply filters — all through query parameters on the `raw` URL. For content editors who aren't comfortable constructing transformation URLs manually, the recommended approach is to use the image's default `url` (which includes the default format) and handle sizing through CSS in the page or block template. The transformation URL approach is most useful for developers authoring content or for editorial workflows where specific image treatments are part of the content itself. --- ## Images in the Content API When consuming images through the content API, image data is included inline in the content records that reference them. ### Image Object Structure Every image field returns an object with this shape: ```typescript { url: 'https://img.sleekcms.com/az41/m7urgy6z.webp', // CDN URL with default format raw: 'https://img.sleekcms.com/az41/m7urgy6z', // Base URL for custom transformations alt: 'Description of the image', // Alt text set by editors source: 'unsplash', // Source platform (null for uploads) light: null, // Light theme variant (URL or null) dark: null // Dark theme variant (URL or null) } ``` Use `url` for direct rendering with the default format. Use `raw` to construct custom transformation URLs by appending query parameters. ### Retrieving Handled Images Images with assigned handles are accessible directly through the `getImage()` method: ```typescript const logo = client.getImage('site-logo'); // { url: '...', raw: '...', alt: 'Company Logo' } const ogImage = client.getImage('og-default'); // { url: '...', raw: '...', alt: 'Default social preview' } ``` ### Building Transformation URLs Your frontend code constructs transformation URLs by appending parameters to the `raw` URL: ```typescript const page = client.getPage('/blog/hello-world'); // Hero banner at 1200×600 in WebP const heroUrl = `${page.image.raw}?w=1200&h=600&fit=cover&fmt=webp`; // Thumbnail at 400×300 const thumbUrl = `${page.image.raw}?w=400&h=300&fit=cover&fmt=webp`; // Retina-ready social preview const ogUrl = `${page.image.raw}?w=1200&h=630&dpr=2&fmt=jpg&q=85`; // Blurred background const bgUrl = `${page.image.raw}?w=1920&blur=20&q=40`; ``` ### Multiple Image Fields When an image field is configured as **multiple**, the API returns an array of image objects: ```typescript const product = client.getPage('/products/widget'); // product.gallery → [ // { url: '...', raw: '...', alt: 'Front view' }, // { url: '...', raw: '...', alt: 'Side view' }, // { url: '...', raw: '...', alt: 'Detail shot' } // ] ``` --- ## What's Next - [Media Management](/media) — Overview of images, videos, and documents in SleekCMS. - [File and Document Assets](/media/files) — Managing PDFs, documents, and downloadable files. - [Content Field Types](/model/fields) — Image fields and other media field types in your content models. - [Template Context and Data Access](/builder/code/context) — The `src()`, `img()`, `picture()`, and `svg()` helpers in detail. - [Image Processing Configuration](/settings/config/imgix) — Configuring default image processing settings for your site. - [Content API](/publish/api) — Retrieving images and content through the API. - [@sleekcms/client](/publish/api/client) — The `getImage()` method and image handling in the JavaScript client. SleekCMS includes an integrated static site builder that turns your content and templates into a fully deployable website — without requiring you to set up build tools, servers, databases, or Git repositories. Everything runs in the cloud. You bind EJS templates to your content models, the builder compiles content and templates into static HTML, and the result is available for instant preview or deployment to any static hosting provider. This page explains the architecture, build pipeline, and key capabilities of the site builder. Implementation details for templates, data access, configuration, and deployment are covered in the linked sub-pages. --- ## Architecture The site builder follows a JAMstack approach: static HTML pages are pre-generated from structured content and templates, then served from a CDN. There is no application server processing requests at runtime. The pipeline has three layers: **Content** — Your models (pages, entries, blocks) and their instances, managed through the SleekCMS editor. This is your data layer. **Templates** — EJS templates bound to each model. These define how content renders to HTML. A page model gets a template, a block model gets a template, an entry model gets a template. Pages can also have a shared layout template that wraps the page template. **Output** — Static HTML files, one per page record, plus your CSS, JavaScript, and other assets. The output is a self-contained static site compatible with any hosting provider. ``` Content (Models + Instances) │ ▼ Templates (EJS bound to models) │ ▼ Static Site (HTML + CSS + JS + Assets) │ ▼ Preview / Deploy ``` → [Integrated JAMstack](/builder/jamstack) --- ## Templates and Models Every model type — page, entry, and block — can be bound to an EJS template. The template defines the HTML output for instances of that model. ### Page templates Each page model has its own EJS template. When the builder processes a page record, it renders the page template with the record's content data. ### Layout template Pages can share a common layout template that wraps the page template. The layout typically defines the outer HTML shell — ``, ``, navigation, footer — while the page template fills in the body content. This gives you consistent site chrome without duplicating markup across page templates. ### Block templates Each block model has its own template. When a page contains a dynamic block field, the builder renders each block instance using the block's template and injects the output into the page. This is how composable layouts work at the template level — the page template delegates rendering of each section to the relevant block template. ### Entry templates Entry models can also have templates. This is useful when an entry needs to render as a reusable fragment — a team member card, an author bio component — that can be included wherever the entry is referenced. → [Model Templates](/builder/code/main) → [Layout](/layout) --- ## Data Flow The entire content of your site is available as a single payload during the build. The payload has this top-level structure: ``` { pages: [ { ... }, { ... }, ... ], entries: { ... }, ... } ``` Each page and entry record in the payload is a nested content structure that mirrors the model definition — groups, collections, blocks, and references are all resolved into nested JSON. ### The `item` variable When a template renders, the current record's data is available as `item`. If you're rendering a page template, `item` is the page record. If you're rendering a block template, `item` is the block instance. If you're rendering an entry template, `item` is the entry record. ### Global content access In addition to `item`, templates have access to the full site content — `entries`, `pages`, `images`, `options` — along with helper methods for searching, filtering, and rendering content. This means any template can reference any content in the site, not just the current record. For example, a blog post page template has direct access to the current post via `item`, but can also look up all author entries, query other pages, or pull in site-wide settings — all without additional API calls. → [Template Context and Data Access](/builder/code/context) --- ## How Pages Are Generated The builder creates one HTML file per page record. The output path depends on the page model type: **Static pages** generate a single file at the path defined in the model. A page model with path `/about` produces `about/index.html`. **Page collections** generate one file per record, using the slug field to differentiate. A blog page model with path `/blog` and records slugged `hello-world` and `nextjs-tips` produces `blog/hello-world/index.html` and `blog/nextjs-tips/index.html`. The mapping is direct: one page record in the content payload equals one HTML file in the output. --- ## Preview and Iteration The site builder supports two preview modes: **Single-page preview** — Generates and renders a single page. This is fast and lets you iterate on content or template changes with near-instant feedback. You can edit content in the editor and see the rendered result side by side. **Full-site preview** — Generates the entire static site. Use this to verify cross-page navigation, global layout changes, or to review the complete site before deployment. Both modes run entirely in the cloud. There is no external build server, no CI pipeline, no waiting for a deployment to complete before you can see your changes. --- ## Assets You can add JavaScript and CSS files through the SleekCMS interface. These files become available to include in your layout templates or any other template where you need them. This is how you add client-side interactivity, custom styles, third-party libraries, or any static assets your site requires. → [Assets and Static Files](/builder/assets) --- ## Styling The site builder includes zero-setup Tailwind CSS support. You can use Tailwind utility classes in your templates without configuring a build pipeline or installing dependencies. → [Tailwind and Styling](/builder/config/tailwind) --- ## Forms SleekCMS provides a built-in form backend for your static sites. You can add contact forms, lead capture, or any form-based interaction without setting up a separate form processing service. → [Form Backend](/builder/forms) --- ## Deployment The generated static site can be deployed in two ways: **Download as ZIP** — Export the entire site as a ZIP archive. The output is a standard static site structure compatible with any hosting provider — Netlify, Cloudflare Pages, Vercel, AWS S3, or any service that serves static files. **Direct deployment** — Deploy from within SleekCMS to supported hosting providers including Netlify, Vercel, and surge.sh. No Git repository required. In both cases, the output is a static site with no server-side runtime dependencies. It works anywhere that serves HTML. → [Deploying Sites](/builder/deploy) --- ## What the Site Builder Is Not A few clarifications for developers coming from other tools: **It is not a frontend framework.** The builder generates static HTML from EJS templates. It doesn't ship a client-side framework, hydration layer, or SPA router. If you need React, Vue, or similar, use the [Content API](/publish/api) with your own frontend instead. **It is not a Git-based workflow.** There are no repositories, branches, or pull requests. Templates and assets are managed through the SleekCMS interface (or an external IDE connected to the platform). Changes are applied directly. **It is not a build pipeline you configure.** There is no `package.json`, no `webpack.config.js`, no build scripts. The builder handles compilation, asset bundling, and output generation internally. → [Online Coding Environment](/builder/code) → [Using VS Code / Cursor IDE](/builder/code/ide) → [Build Configuration and Environment](/builder/config) --- ## What's Next - [Integrated JAMstack](/builder/jamstack) — The static generation philosophy and how SleekCMS implements it. - [Model Templates](/builder/code/main) — Binding EJS templates to page, entry, and block models. - [Layout](/layout) — The common layout wrapper template for pages. - [Template Context and Data Access](/builder/code/context) — The `item` variable, global content access, and helper methods. - [Online Coding Environment](/builder/code) — The browser-based code editor for templates and assets. - [Using VS Code / Cursor IDE](/builder/code/ide) — Connecting an external IDE for local development. - [Assets and Static Files](/builder/assets) — Managing JavaScript, CSS, and other static files. - [Build Configuration](/builder/config) — Environment settings and build options. - [Tailwind and Styling](/builder/config/tailwind) — Zero-setup Tailwind CSS support. - [Form Backend](/builder/forms) — Processing form submissions from static sites. - [Deploying Sites](/builder/deploy) — Download, Netlify, Vercel, surge.sh, and other deployment options. The SleekCMS site builder turns your content and templates into a static website — with no servers, databases, Git repositories, or deployment pipelines to manage. This page is the technical reference for how that process works: how templates are structured, how they compose into pages, what data and helpers are available during rendering, and how the final HTML output is produced. Whether you're writing your first page template or designing a block library for a multi-page marketing site, this is the page that explains what's available to you and how the pieces connect. --- ## How Templates Generate a Static Site The site builder produces a complete static site by processing every page record through a pipeline of templates. Understanding this pipeline helps you see where each template type fits and what data flows through it. ### The Build Pipeline When you build your site, the builder iterates over every page record in your content and produces one HTML file per page. For each page, the rendering follows this sequence: **1. Page template** — The builder takes the page record and renders it through the page model's EJS template. The page record's field data becomes the `item` variable. If the page has dynamic block fields, the page template calls `render()` to delegate each block to its own template. The result is the page's body HTML. **2. Block templates** — When `render()` is called on a dynamic block field, the builder iterates over the block instances. For each block, it looks up the block model's template and renders it with the block's field data as `item`. The rendered HTML for all blocks is concatenated and returned to the page template. **3. Entry templates** — If a page or block calls `render()` on a referenced entry, the builder renders the entry model's template with the entry's data as `item`. This is optional — entries without templates are accessed through their field data directly. **4. Layout template** — After the page template finishes rendering, the builder wraps its output with the layout template. The page's rendered HTML becomes available as the `main` variable inside the layout. The layout adds the outer HTML shell — ``, ``, navigation, footer — around the page content. ``` For each page record: ┌──────────────────────────────────────────────────┐ │ Layout Template │ │ ├── , , nav, footer │ │ └── <%- main %> │ │ └── Page Template │ │ ├── Fixed fields (title, meta, hero) │ │ └── <%- render(item.sections) %> │ │ ├── hero.ejs → item = {...} │ │ ├── features.ejs → item = {...} │ │ └── cta.ejs → item = {...} │ └──────────────────────────────────────────────────┘ ↓ about/index.html ``` ### Global Content Availability Every template in the pipeline — layout, page, block, entry — has access to the **entire site's content**, not just the current record. This means a block template can look up entries, a page template can query other pages for related content links, and the layout template can pull in navigation data from an admin-only entry. The current record is always available as `item`, while the full site content is accessible through helper methods like `getEntry()`, `getPages()`, and `getContent()`. ### Output The builder produces one HTML file per page record. Static pages generate a single file at their defined path (`/about` → `about/index.html`). Page collections generate one file per record using the slug (`/blog` + `hello-world` → `blog/hello-world/index.html`). The complete set of HTML files, combined with your CSS, JavaScript, and other assets, forms a self-contained static site ready for deployment. → [Site Builder](/builder) → [Integrated JAMstack](/builder/jamstack) --- ## How EJS Templates Work SleekCMS templates use [EJS](https://ejs.co/) (Embedded JavaScript) syntax. EJS was chosen because it gives you the full power of JavaScript directly inside your templates — not just variable interpolation and basic loops, but the entire language. You can use array methods like `filter()`, `map()`, and `sort()` to transform content, write conditional logic of any complexity, declare variables and helper functions, and work with dates, strings, and objects using standard JavaScript. There's no limited template DSL to learn — if you can write JavaScript, you can write EJS templates. There are three tag types you'll use: **`<%= expression %>`** — Outputs the value with HTML escaping. Use for text content where you want to prevent HTML injection. **`<%- expression %>`** — Outputs the value without escaping. Use for trusted HTML output — rendered blocks, image elements, rich text fields, and helper functions that return HTML. **`<% code %>`** — Executes JavaScript without outputting anything. Use for logic — loops, conditionals, variable assignments. ```ejs

<%= item.title %>

<%- item.body %>
<%- render(item.sections) %> <%- img(item.hero, '800x400') %> <% if (item.featured) { %> Featured <% } %> <% const posts = getPages('/blog'); %> <% posts.forEach(post => { %> <%= post.title %> <% }); %> ``` --- ## The `item` Variable When a template renders, the current record's data is available as `item`. This is the primary way you access content in any template. **In a page template**, `item` is the page record. It contains all the fields defined in the page model, plus system properties: - `item._path` — The full path of the page (e.g., `/about`, `/blog/hello-world`) - `item._slug` — The slug portion of the path, present on collection pages (e.g., `hello-world`) - `item._meta.updated_at` — Timestamp of the last update ```ejs

<%= item.title %>

<%= item.image.alt %>
<%- item.content %>
``` **In a block template**, `item` is the block instance. It contains the block's field data plus `item._meta.name`, which is the block model's handle. ```ejs

<%= item.heading %>

<%= item.subheading %>

<%= item.cta_label %>
``` **In an entry template**, `item` is the entry record with its field data and `item._meta.updated_at`. ```ejs
<%- img(item.headshot, '80x80') %> <%= item.name %>

<%= item.bio %>

``` --- ## Direct Content Access In addition to `item`, every template has direct access to the full site content through these top-level variables: ### `pages` An array of all page records in the site. Each page object contains its fields and system properties (`_path`, `_slug`, `_meta`). ```ejs <% pages.forEach(page => { %> <%= page.title %> <% }); %> ``` ### `entries` An object containing all entries, keyed by handle. Single entries are objects; entry collections are arrays of objects. ```ejs

<%- entries.footer.copyright_text %>

<% entries.authors.forEach(author => { %> <%= author.name %> <% }); %> ``` ### `images` An object containing all images that have a handle, keyed by that handle. Each image has `url` and `alt` properties. ```ejs <%- images['site-logo'].alt %> ``` ### `options` An object containing all option sets that have a handle, keyed by that handle. Each option set is an array of `{ label, value }` pairs. ```ejs <% options.categories.forEach(cat => { %> <%= cat.label %> <% }); %> ``` > **Recommendation:** Use the helper methods (`getEntry()`, `getPage()`, `getImage()`, `getOptions()`) instead of accessing these variables directly. The helpers provide a cleaner, more readable API and are consistent with the `@sleekcms/client` library, making it easier to switch between site builder templates and headless API usage. --- ## Rendering Helpers These helpers handle template rendering and HTML generation — the core tools for composing pages from blocks, rendering images, and building navigation. ### `main` The rendered output of the page model's template. This is only relevant inside the **layout template**, where it represents the page body content that the layout wraps. ```ejs
<%- main %>
``` The layout template provides the HTML shell. `main` is where the page template's output gets inserted. You never use `main` inside page, block, or entry templates — only in the layout. ### `render(val, separator?)` Renders a block, an entry, or an array of blocks/entries using their associated templates. This is how composable layouts work — the page template delegates rendering to each block's own template. **Parameters:** - `val` — A single block/entry object or an array of block/entry objects - `separator` (optional) — A string inserted between rendered items when `val` is an array (default: `""`) **Returns:** The rendered HTML string. ```ejs <%- render(item.sections) %> <%- render(item.hero_block) %> <%- render(item.sections, '
') %> <%- render(item.author) %> ``` When you call `render(item.sections)`, the builder iterates over every block instance in the `sections` field, looks up each block model's EJS template, renders it with the block's field data as `item`, and concatenates the HTML output. The page template doesn't need to know what types of blocks are present or how to render them. ### `src(obj, attr)` Generates an optimized image URL with resizing parameters. Use this when you need just the URL — for `background-image` styles, custom `` elements, or anywhere you need the raw URL string. **Parameters:** - `obj` — An image object (e.g., `item.image`, `item.hero`) - `attr` — Dimensions as a string `"WxH"` or an object with `{ w, h, size, type, fit }` properties **Returns:** A URL string. ```ejs <%= item.hero.alt %>
``` If the image object is invalid or missing, `src()` returns a placeholder URL (`https://placehold.co/{size}.png`), so your templates won't break from missing images. ### `img(obj, attr)` Generates a complete `` HTML element. A convenience wrapper around `src()` that also handles the `alt` attribute and supports `class` and `style` properties. **Parameters:** - `obj` — An image object - `attr` — Dimensions as a string `"WxH"` or an object with `{ w, h, size, type, fit, class, style }` properties **Returns:** An `` HTML string. ```ejs <%- img(item.hero, '800x600') %> <%- img(item.hero, { size: '800x600', class: 'hero-img', style: 'object-fit: cover' }) %> <%- img(null, '400x300') %> ``` Use `img()` when you want a simple image element. Use `src()` when you need the URL for a background image or a custom element structure. ### `picture(obj, attr)` Generates a `` element with support for dark/light theme variants. If the image has `dark` or `light` variant images assigned, `picture()` automatically includes `` elements with `prefers-color-scheme` media queries. **Parameters:** - `obj` — An image object with optional `dark`/`light` variant properties - `attr` — Dimensions as a string `"WxH"` or an object with `{ w, h, size, type, fit, class, style }` properties **Returns:** A `` HTML string. ```ejs <%- picture(item.hero, '800x600') %> <%- picture(item.logo, '200x50') %> <%- picture(item.brand_logo, '300x100') %> ``` Use `picture()` for logos, brand assets, or any image that should adapt to the user's color scheme preference. ### `svg(obj, attr?)` Inlines an SVG image directly into the HTML with optional attributes applied to the root `` element. This gives you full control over SVG styling through CSS, unlike an `` tag which treats the SVG as an opaque image. The SVG markup is sanitized before inlining to remove potentially malicious content such as embedded scripts, event handlers, and external references. **Parameters:** - `obj` — An image object whose URL points to an SVG file - `attr` (optional) — An object of HTML attributes to apply to the `` element **Returns:** The inline SVG markup. ```ejs <%- svg(item.icon) %> <%- svg(item.logo, { width: 120, height: 40, class: 'logo' }) %> ``` ### `path(obj)` Returns the URL path for a page object. This is a convenience wrapper that reads `obj._path`, returning `'#'` if the page is not found. ```ejs Current Page <% getPages('/blog').forEach(post => { %> <%= post.title %> <% }); %> ``` --- ## Content Access Helpers These methods provide a clean, readable API for querying content from any template. They mirror the methods available in `@sleekcms/client`, so the same mental model applies whether you're writing templates for the site builder or fetching content through the API. ### `getContent(search?)` Returns all site content, or filters it with a [JMESPath](https://jmespath.org/) query. **Parameters:** - `search` (optional) — A JMESPath expression to filter content **Returns:** The matched content, or the full content object `{ pages, entries, images, options }`. ```ejs <% const content = getContent(); %> <% const allPages = getContent('pages'); %> <% const footer = getContent('entries.footer'); %> <% const aboutPage = getContent('pages[?_path==`/about`] | [0]'); %> <% const featuredPosts = getContent('pages[?featured==`true`]'); %> ``` ### `getPages(path, opts?)` Returns all pages whose path starts with the given prefix. **Parameters:** - `path` — The path prefix to match - `opts` (optional) — Options object with `{ collection: boolean }` to filter to collection pages only **Returns:** An array of page objects. ```ejs <% const posts = getPages('/blog'); %> <% const blogPosts = getPages('/blog', { collection: true }); %> <% posts.forEach(post => { %>

<%= post.title %>

<%= post.published_date %>

<% }); %> ``` ### `getPage(path)` Returns a single page by exact path match. **Parameters:** - `path` — The exact path to match **Returns:** The page object, or `undefined` if not found. ```ejs <% const about = getPage('/about'); %> <% if (about) { %> <%= about.title %> <% } %> <% const home = getPage('/'); %> ``` ### `getEntry(handle)` Returns an entry by its handle. Single entries return an object; entry collections return an array of objects. **Parameters:** - `handle` — The entry handle **Returns:** An entry object, an array of entry objects, or `undefined`. ```ejs <% const footer = getEntry('footer'); %>

<%- footer.copyright_text %>

<% const authors = getEntry('authors'); %> <% authors.forEach(author => { %>
<%= author.name %>
<% }); %> ``` ### `getSlugs(path)` Returns an array of slug strings for pages under a given path. Useful for generating navigation or listing collection items. **Parameters:** - `path` — The base path to extract slugs from **Returns:** An array of strings. ```ejs <% const slugs = getSlugs('/blog'); %> ``` ### `getImage(handle)` Returns an image by its handle. **Parameters:** - `handle` — The image handle **Returns:** An image object `{ url, alt, ... }` or `undefined`. ```ejs <% const logo = getImage('site-logo'); %> <% if (logo) { %> <%= logo.alt %> <% } %> ``` ### `getOptions(handle)` Returns an option set — an array of label/value pairs. **Parameters:** - `handle` — The option set handle **Returns:** An array of `{ label, value }` objects, or `undefined`. ```ejs <% const categories = getOptions('blog-categories'); %>
    <% categories?.forEach(cat => { %>
  • <%= cat.label %>
  • <% }); %>
``` --- ## Head Injection Methods These methods add elements to the page's `` section and script area from within any template — page, block, entry, or layout. Duplicate entries are automatically prevented based on content hashing. ### `title(str)` Sets the page `` element. ```ejs <% title(item.title + ' | My Website') %> <!-- Output: <title>About Us | My Website --> ``` ### `meta(obj)` Adds a `` tag to the head. ```ejs <% meta({ name: 'author', content: 'John Doe' }) %> <% meta({ property: 'og:type', content: 'website' }) %> <% meta({ name: 'description', content: item.meta_description }) %> ``` ### `link(value, order?)` Adds a `` tag. Accepts a string URL (auto-detects type by extension) or an object with explicit attributes. The optional `order` parameter controls output order — lower numbers appear first. ```ejs <% link('/css/custom.css') %> <% link('/feed.xml') %> <% link('/favicon.ico') %> <% link({ rel: 'canonical', href: 'https://example.com' + path(item) }) %> <% link({ rel: 'preconnect', href: 'https://fonts.googleapis.com' }) %> ``` ### `style(value, order?)` Adds inline CSS to the head as a ` --> ``` ### `script(value, order?)` Adds a script to the page (appended to ``). Accepts a `.js` URL string for an external script, a code string for inline JavaScript, or an object with `src`/`content` and additional attributes. ```ejs <% script('/js/analytics.js') %> <% script('console.log("Page loaded");') %> <% script({ src: '/js/app.js', defer: true }) %> <% script({ src: '/js/module.js', type: 'module' }) %> ``` --- ## Putting It All Together These examples show how the template context pieces fit together in real templates. ### Layout Template The layout wraps every page with the site's HTML shell. It uses `main` to inject the page template output and head injection methods for metadata. ```ejs
<% const nav = getEntry('header'); %> <%- picture(getImage('site-logo'), '150x40') %>
<%- main %>
<% const footer = getEntry('footer'); %>

<%- footer.copyright_text %>

<% footer.socials.forEach(social => { %> <% }); %>
``` ### Blog Post Page Template A page template that combines fixed fields with dynamic block sections, SEO metadata, and related post navigation. ```ejs <% title(item.title + ' | My Blog') %> <% meta({ name: 'description', content: item.seo?.description || '' }) %> <% link({ rel: 'canonical', href: 'https://example.com' + path(item) }) %>

<%= item.title %>

<% if (item.category && item.category.length) { %>
<% item.category.forEach(cat => { %> <%= cat %> <% }); %>
<% } %>
<% if (item.image) { %> <%- img(item.image, { size: '1200x600', class: 'featured-image' }) %> <% } %>
<%- item.content %>
``` ### Landing Page with Dynamic Blocks A page model that combines a fixed hero area with composable block sections below. ```ejs <% title(item.title + ' | My Site') %>

<%= item.title %>

<%= item.tagline %>

<%- render(item.sections) %> ``` Each block in `item.sections` is rendered by its own template. For example, a "Features Grid" block template: ```ejs

<%= item.heading %>

<% item.items.forEach(feature => { %>
<%- img(feature.icon, '48x48') %>

<%= feature.title %>

<%= feature.description %>

<% }); %>
``` And a "CTA Banner" block template: ```ejs

<%= item.heading %>

<%= item.body_text %>

<%= item.button_label %>
``` ### Dropdown-Driven Layout Variants Using option set values from dropdown fields to control rendering: ```ejs
<% if (item.layout === 'grid') { %>
<% item.features.forEach(f => { %>
<%= f.title %>
<% }); %>
<% } else if (item.layout === 'list') { %>
    <% item.features.forEach(f => { %>
  • <%= f.title %> — <%= f.description %>
  • <% }); %>
<% } %>
``` ### Entry References in a Blog Post When a page references an entry, the entry data is available inline: ```ejs

<%= item.title %>

<%- item.content %> <% if (item.author) { %>
<%- img(item.author.headshot, '64x64') %> <%= item.author.name %>

<%= item.author.bio %>

<% } %>
``` If the author entry model has its own template, you can delegate rendering instead: ```ejs <% if (item.author) { %> <%- render(item.author) %> <% } %> ``` --- ## Template Context Summary | Context | Type | Available In | Description | |---|---|---|---| | `item` | Object | All templates | Current page, block, or entry record | | `main` | String | Layout only | Rendered page template output | | `pages` | Array | All templates | All page records | | `entries` | Object | All templates | All entries keyed by handle | | `images` | Object | All templates | All handled images | | `options` | Object | All templates | All handled option sets | | Helper | Returns | Purpose | |---|---|---| | `render(val)` | HTML string | Render block/entry through its template | | `src(obj, attr)` | URL string | Generate optimized image URL | | `img(obj, attr)` | HTML string | Generate `` element | | `picture(obj, attr)` | HTML string | Generate `` with theme variants | | `svg(obj, attr?)` | HTML string | Inline SVG with attributes | | `path(obj)` | String | Get URL path for a page | | `getContent(search?)` | Any | Query all content with JMESPath | | `getPages(path, opts?)` | Array | Find pages by path prefix | | `getPage(path)` | Object | Find single page by exact path | | `getEntry(handle)` | Object/Array | Get entry by handle | | `getSlugs(path)` | Array | Extract slugs from collection pages | | `getImage(handle)` | Object | Get a handled image | | `getOptions(handle)` | Array | Get an option set | | `title(str)` | void | Set page `` | | `meta(obj)` | void | Add `<meta>` tag | | `link(value, order?)` | void | Add `<link>` tag | | `style(value, order?)` | void | Add inline `<style>` | | `script(value, order?)` | void | Add `<script>` to body | --- ## What's Next - [Model Templates](/builder/code/main) — Binding EJS templates to page, entry, and block models. - [Layout](/layout) — The common layout wrapper template for pages. - [Site Builder](/builder) — The integrated coding environment for templates and assets. - [Assets and Static Files](/builder/assets) — Managing JavaScript, CSS, and other static files. - [Content Field Types](/model/fields) — The full set of field types that shape template data. - [Dynamic Blocks](/model/fields/dynamic) — Configuring composable block sections. - [@sleekcms/client](/publish/api/client) — The JavaScript/TypeScript client with matching helper methods. Every EJS template in the site builder — page templates, block templates, entry templates, and the layout template — executes with a rich context object. This context gives your template access to the current record's data, the full site content, helper methods for querying and rendering content, image utilities, and injection methods for adding resources to the page head. This page is the complete reference for everything available inside an EJS template. It covers the content payload structure, the `item` variable, content query methods, rendering helpers, image and media utilities, head injection methods, and how these pieces fit together in practice. --- ## The Content Payload During a build, the entire site's content is loaded as a single JSON payload. This payload is what every template draws from — whether you're accessing the current record, looking up an entry, querying pages, or iterating over option sets. The payload has this top-level structure: ```json { "pages": [ ... ], "entries": { ... }, "images": { ... }, "options": { ... } } ``` **`pages`** — An array of all page records across all page models. Each page object contains the fields defined by its model, plus system properties like `_path`, `_slug`, and `_meta`. **`entries`** — An object keyed by entry model handle. Single entries are objects; entry collections are arrays. Each entry contains its model's fields plus a `_meta` property. **`images`** — An object of named images uploaded at the site level (distinct from images attached to individual content records). **`options`** — An object of option sets keyed by handle. Each option set is an array of `{ label, value }` pairs. You don't interact with this payload directly. Instead, you access it through the `item` variable and the helper methods described in this page. But understanding the payload structure helps you reason about what data is available and how it's organized. --- ## The `item` Variable When a template renders, the current record's data is available as `item`. This is the primary variable you work with in every template. What `item` contains depends on which template is rendering: **In a page template** — `item` is the page record. It contains all the fields defined in the page model, plus system properties. **In a block template** — `item` is the block instance. It contains the fields defined in the block model. When a page has a dynamic block field and the builder renders each block, each block template receives its own block data as `item`. **In an entry template** — `item` is the entry record, with all the fields defined in the entry model. **In the layout template** — `item` is the same page record that the page template receives. The layout wraps the page, and both share the same `item`. ### System Properties Every page record includes system properties prefixed with an underscore: | Property | Description | Present On | |---|---|---| | `_path` | The full URL path of the page (e.g., `/blog/hello-world`) | All pages | | `_slug` | The slug segment for collection pages (e.g., `hello-world`) | Collection pages | | `_meta.key` | Internal record identifier | All records | | `_meta.created_at` | ISO 8601 creation timestamp | All records | | `_meta.updated_at` | ISO 8601 last-update timestamp | All records | ### Field Data Shapes The shape of each field in `item` mirrors how it's defined in the model: **Text, Number, Boolean, Date, Code, Video** — Primitive values (string, number, boolean). **Markdown, Rich Text** — HTML strings. Markdown is pre-rendered to HTML in the payload, so you don't need a parser. **Dropdown** — The selected option's value as a string (not the label). **Image** — An object with `url`, `raw`, `alt`, `source`, `light`, `dark`, and `_meta` properties. **Reference (one-to-one)** — The full referenced entry object, resolved inline. You access the entry's fields directly — `item.author.name`, not a separate lookup. **Reference (one-to-many)** — An array of full entry objects. **Group** — A nested object. Fields inside a group are accessed with dot notation: `item.seo.meta_title`. **Collection** — An array of objects, each with the collection's sub-fields. **Dynamic Blocks** — An array of block objects, each with a `_type` property identifying the block model and the block's field data. **Location** — An object with coordinates and a static map link. ```ejs <!-- Primitive fields --> <h1><%= item.title %></h1> <time datetime="<%= item.published_date %>"><%= item.published_date %></time> <!-- Rich text / Markdown (already HTML — use unescaped output) --> <div class="content"><%- item.body %></div> <!-- Image object --> <img src="<%= item.hero_image.url %>" alt="<%= item.hero_image.alt %>"> <!-- Resolved reference --> <span>By <%= item.author.name %></span> <!-- Group (nested object) --> <meta name="description" content="<%= item.seo.meta_description %>"> <!-- Collection (array of objects) --> <% item.faqs.forEach(faq => { %> <details> <summary><%= faq.question %></summary> <p><%- faq.answer %></p> </details> <% }); %> <!-- Dropdown value used for conditional logic --> <section class="theme-<%= item.color_theme %>"> ``` --- ## Content Query Methods These methods let any template query the full site content — pages, entries, images, and options — regardless of which record is currently rendering. They're available directly in the template context, no imports needed. ### `getContent(search?)` Returns the full content payload, or filters it with a [JMESPath](https://jmespath.org/) query. This is the most flexible content access method — you can reach any part of the payload with the right query. ```ejs <!-- Full payload --> <% const content = getContent(); %> <!-- All pages --> <% const allPages = getContent('pages'); %> <!-- A specific entry by handle --> <% const footer = getContent('entries.footer'); %> <!-- Filter pages with JMESPath --> <% const aboutPage = getContent('pages[?_path==`/about`] | [0]'); %> <!-- Pages matching a condition --> <% const featuredPosts = getContent('pages[?featured==`true`]'); %> ``` ### `getPage(path)` Finds a single page by exact path. Returns the page object, or `undefined` if no page matches. ```ejs <% const about = getPage('/about'); %> <% if (about) { %> <a href="<%- path(about) %>"><%= about.title %></a> <% } %> <% const home = getPage('/'); %> ``` ### `getPages(path, opts?)` Finds all pages whose path starts with the given prefix. Returns an array. Pass `{ collection: true }` to return only collection pages (pages with a slug), excluding the index page at the base path. ```ejs <!-- All pages under /blog --> <% const posts = getPages('/blog/'); %> <!-- Only collection records (excludes the /blog index page) --> <% const blogPosts = getPages('/blog/', { collection: true }); %> ``` ### `getEntry(handle)` Gets an entry by its model handle. Returns the entry object for single entries, or an array of entry objects for entry collections. Returns `undefined` if not found. ```ejs <% const footer = getEntry('footer'); %> <footer> <p><%- footer.copyright_text %></p> <nav> <% footer.links.forEach(link => { %> <a href="<%- link.url %>"><%- link.label %></a> <% }); %> </nav> </footer> <% const nav = getEntry('main-navigation'); %> ``` ### `getSlugs(basePath)` Extracts slugs from collection pages under a path. Returns an array of slug strings. ```ejs <% const blogSlugs = getSlugs('/blog'); %> <!-- Returns: ['hello-world', 'nextjs-tips', 'latest-news'] --> ``` ### `getImage(name)` Gets a named site-level image by key. Returns the image object or `undefined`. ```ejs <% const logo = getImage('site-logo'); %> <img src="<%- src(logo, '200x50') %>" alt="Site Logo"> <% const hero = getImage('homepage-hero'); %> <% if (hero) { %> <div style="background-image: url('<%- src(hero, '1920x1080') %>')"></div> <% } %> ``` ### `getOptions(name)` Gets an option set by handle. Returns an array of `{ label, value }` pairs, or `undefined`. ```ejs <% const countries = getOptions('countries'); %> <select name="country"> <% countries?.forEach(opt => { %> <option value="<%- opt.value %>"><%- opt.label %></option> <% }); %> </select> <% const categories = getOptions('blog-categories'); %> <ul class="categories"> <% categories?.forEach(cat => { %> <li data-value="<%- cat.value %>"><%- cat.label %></li> <% }); %> </ul> ``` --- ## Rendering Helpers ### `render(val, separator?)` Renders block content or entry templates. This is the method that makes composable layouts work — it delegates rendering to the appropriate block or entry template. When you pass a single block object, `render()` looks up the block model's template and renders it with the block's data as `item`. When you pass an array (a dynamic block field), it renders each block in sequence and concatenates the HTML output. An optional `separator` string is inserted between rendered blocks. ```ejs <!-- Render all sections in a dynamic block field --> <%- render(item.sections) %> <!-- Render with a separator between blocks --> <%- render(item.sections, '<hr>') %> <!-- Render a single block field --> <%- render(item.sidebar_block) %> ``` The `render()` call is what connects page templates to block templates. A page template doesn't need to know how to render a hero banner or a pricing table — it calls `render(item.sections)` and each block's own template handles the rest. → [Dynamic Blocks](/model/fields/dynamic) → [Model Templates](/builder/code/main) ### `path(obj)` Returns the URL path for a page object. If the object doesn't have a `_path` property, returns `'#'`. ```ejs <a href="<%- path(item) %>">Current Page</a> <% findPages('/blog/').forEach(post => { %> <a href="<%- path(post) %>"><%= post.title %></a> <% }); %> ``` --- ## Image Utilities SleekCMS provides image helper methods that generate optimized image URLs with resizing parameters and produce ready-to-use HTML elements. These work with the image processing service that handles on-the-fly resizing, format conversion, and optimization. All image utilities accept an image object (the value from an image field) and an `attr` parameter that specifies dimensions and options. ### The `attr` Parameter The `attr` parameter can be either a string shorthand or an object with detailed options: **String shorthand** — A `"WxH"` string like `"800x600"` that sets width and height. **Object form** — An object with any combination of these properties: | Property | Type | Description | |---|---|---| | `w` | number | Width in pixels | | `h` | number | Height in pixels | | `size` | string | Shorthand for width × height (e.g., `'400x300'`) | | `fit` | string | Resize fitting mode: `cover`, `contain`, `fill`, `inside`, `outside` | | `type` | string | Output format: `webp`, `avif`, `png`, `jpg` | | `class` | string | CSS class (for `img()` and `picture()` only) | | `style` | string | Inline CSS (for `img()` and `picture()` only) | ### `src(obj, attr)` Generates an optimized image URL with resizing parameters. Returns a URL string. When the image object is invalid or missing, returns a placeholder URL from `placehold.co`. When the image is from Unsplash, it handles Unsplash query parameter conventions automatically. ```ejs <!-- String dimensions --> <img src="<%- src(item.hero, '800x600') %>" alt="<%= item.hero.alt %>"> <!-- Object with options --> <img src="<%- src(item.thumbnail, { w: 400, h: 300, fit: 'cover', type: 'webp' }) %>"> <!-- Size shorthand in object form --> <img src="<%- src(item.thumbnail, { size: '400x300', fit: 'cover' }) %>"> <!-- Placeholder fallback when image is missing --> <img src="<%- src(null, '400x300') %>"> <!-- Output: https://placehold.co/400x300.png --> ``` ### `img(obj, attr)` Generates a complete `<img>` HTML element. Handles `src` URL generation, `alt` attribute escaping, and optional `class` and `style` attributes. Returns an HTML string. Falls back to a placeholder element when the image object is invalid. Use this as a simpler alternative to `picture()` when you don't need dark/light theme variants. ```ejs <!-- Basic img element --> <%- img(item.hero, '800x600') %> <!-- Output: <img src="https://example.com/hero.png?w=800&h=600" alt="Hero description"> --> <!-- With object attributes --> <%- img(item.thumbnail, { w: 400, h: 300, fit: 'cover', type: 'webp' }) %> <!-- With class and style --> <%- img(item.hero, { size: '800x600', class: 'hero-img', style: 'object-fit: cover' }) %> <!-- Placeholder fallback --> <%- img(null, '400x300') %> <!-- Output: <img src="https://placehold.co/400x300.png" alt=""> --> ``` ### `picture(obj, attr)` Generates a `<picture>` element with support for dark and light theme variants. When the image object has `dark` or `light` variant properties, the output includes `<source>` elements with `prefers-color-scheme` media queries for automatic theme switching. Falls back to a simple `<picture>` with a single `<img>` when no variants are present. ```ejs <!-- Basic picture element --> <%- picture(item.hero, '800x600') %> <!-- Output: <picture><img src="..." alt="..."></picture> --> <!-- Image with dark mode variant --> <%- picture(item.logo, '200x50') %> <!-- Output when dark variant exists: <picture> <source srcset="https://.../logo-dark.png?w=200&h=50" media="(prefers-color-scheme: dark)"> <img src="https://.../logo.png?w=200&h=50" alt="Logo"> </picture> --> <!-- Image with both dark and light variants --> <%- picture(item.brand_logo, '300x100') %> <!-- Output: <picture> <source srcset="https://.../logo-dark.png" media="(prefers-color-scheme: dark)"> <source srcset="https://.../logo-light.png" media="(prefers-color-scheme: light)"> <img src="https://.../logo.png" alt="Brand Logo"> </picture> --> <!-- With class and style --> <%- picture(item.logo, { size: '200x50', class: 'logo', style: 'max-width: 100%' }) %> ``` ### `svg(obj, attr?)` Inlines an SVG file from a URL with optional custom attributes applied to the root `<svg>` element. This gives you full CSS control over the SVG's colors, sizing, and animations — something not possible with `<img>` tags. Returns the inline SVG markup. Returns an empty SVG with an error attribute if the object is invalid or the URL doesn't point to an `.svg` file. ```ejs <!-- Inline SVG --> <%- svg(item.icon) %> <!-- Output: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M12 2L2 7l10 5 10-5-10-5z"/> </svg> --> <!-- With custom attributes --> <%- svg(item.logo, { width: 120, height: 40, class: 'logo' }) %> <!-- Output: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 60" width="120" height="40" class="logo"> <text x="10" y="40">My Logo</text> </svg> --> ``` --- ## Head Injection Methods These methods add resources to the page's `<head>` section (and scripts to the end of `<body>`). They can be called from any template — page, block, entry, or layout — and the builder collects all injected resources and places them in the correct location in the final HTML output. All injection methods (except `title`) support an optional `order` parameter that controls the position of the output. Lower numbers appear first. Duplicate entries (based on content hash) are automatically deduplicated, so calling the same injection twice won't produce duplicate tags. ### `title(value)` Sets the page's `<title>` tag. ```ejs <% title(item.title + ' | My Website') %> <!-- Output: <title>About Us | My Website --> ``` ### `meta(value, order?)` Adds a `` tag. Pass an object with the tag's attributes. ```ejs <% meta({ name: 'author', content: 'John Doe' }) %> <% meta({ property: 'og:type', content: 'website' }) %> ``` ### `link(value, order?)` Adds a `` tag. Accepts a string URL (auto-detects the type based on file extension) or an object with explicit attributes. ```ejs <% link('/css/custom.css') %> <% link('/feed.xml') %> <% link('/favicon.ico') %> <% link({ rel: 'canonical', href: 'https://example.com' + path(item) }) %> <% link({ rel: 'preconnect', href: 'https://fonts.googleapis.com' }) %> ``` ### `style(value, order?)` Adds inline CSS. Pass a string of CSS rules, or an object with a `content` property. ```ejs <% style('.hero { background: #000; color: #fff; }') %> <% style({ content: '.btn { padding: 10px 20px; }' }) %> ``` ### `script(value, order?)` Adds a script to the page, appended to the end of ``. Accepts a string (interpreted as a URL if it ends in `.js`, otherwise as inline code) or an object with `src`, `content`, and other script attributes. ```ejs <% script('/js/analytics.js') %> <% script('console.log("Page loaded");') %> <% script({ src: '/js/app.js', defer: true }) %> <% script({ src: '/js/module.js', type: 'module' }) %> <% script({ content: 'window.config = { debug: true };' }) %> ``` --- ## Client-Compatible Methods The template context methods — `getContent`, `getPage`, `getPages`, `getEntry`, `getSlugs`, `getImage`, and `getOptions` — are designed to be API-compatible with the `@sleekcms/client` JavaScript library. They share the same method names, parameters, and return shapes. This means the same mental model applies whether you're querying content in a site builder template or in a Next.js component using the client library. A call like `getPages('/blog/', { collection: true })` works identically in both contexts. → [@sleekcms/client](/publish/api/client) --- ## Complete Example A page template that combines the context features covered in this page: ```ejs <% title(item.title + ' | My Site') %> <% meta({ property: 'og:title', content: item.title }) %> <% meta({ property: 'og:type', content: 'article' }) %> <% link({ rel: 'canonical', href: 'https://mysite.com' + path(item) }) %>

<%= item.title %>

<% if (item.author) { %> <% } %> <% if (item.hero) { %> <%- picture(item.hero, { size: '1200x600', class: 'hero-image' }) %> <% } %>
<%- render(item.sections) %>
<% const footer = getEntry('footer'); %>

<%- footer.copyright_text %>

<% footer.socials.forEach(social => { %> <% }); %>
``` This template sets the page title and meta tags via injection, renders the current page's author reference, hero image with dark mode support, dynamic block sections, related posts from the same collection, and a footer from an admin-only entry — all using the methods documented on this page. --- ## Quick Reference ### Content | Context | Returns | Purpose | |---|---|---| | `item` | Object | Current record's data | | `pages` | Array | All page records | | `entries` | Object | All entries keyed by model handle | | `images` | Object | Named site-level images | | `options` | Object | Option sets keyed by handle | ### Access Methods | Method | Returns | Purpose | |---|---|---| | `getContent(search?)` | Any | Full payload or JMESPath query result | | `getPage(path)` | Object \| undefined | Single page by exact path | | `getPages(path, opts?)` | Array | Pages matching a path prefix | | `getEntry(handle)` | Object \| Array \| undefined | Entry by model handle | | `getSlugs(basePath)` | string[] | Slug values for a collection | | `getImage(name)` | Object \| undefined | Named site-level image | | `getOptions(name)` | Array \| undefined | Option set by handle | ### Rendering | Method | Returns | Purpose | |---|---|---| | `render(val, separator?)` | string | Render blocks or entry templates | | `path(obj)` | string | URL path for a page object | | `src(obj, attr)` | string | Optimized image URL | | `img(obj, attr)` | string | `` HTML element | | `picture(obj, attr)` | string | `` element with theme variants | | `svg(obj, attr?)` | string | Inline SVG markup | ### Head and Meta | Method | Returns | Purpose | |---|---|---| | `title(value)` | void | Set page `` | | `meta(value, order?)` | void | Add `<meta>` tag | | `link(value, order?)` | void | Add `<link>` tag | | `style(value, order?)` | void | Add inline `<style>` | | `script(value, order?)` | void | Add `<script>` to body | --- ## What's Next - [Model Templates](/builder/code/main) — Binding EJS templates to page, entry, and block models. - [Layout](/layout) — The shared layout template that wraps page output. - [Site Builder](/builder) — The overall build pipeline and preview system. - [Dynamic Blocks](/model/fields/dynamic) — How `render()` powers composable page layouts. - [Content Field Types](/model/fields) — The field types that determine `item` data shapes. - [Content API](/publish/api) — How the same content structure flows through the API. - [@sleekcms/client](/publish/api/client) — The JavaScript client with matching method signatures. # Integrating Content Once your content is modeled and created in SleekCMS, the next step is getting it into your website, app, or frontend. SleekCMS provides three mechanisms for this: a content API for reading your data, publish triggers for automating external workflows, and environments for managing content versions. Together, these give you a complete integration layer. The API delivers your content. Triggers notify your build and deployment systems when content changes. Environments let you control which version of content is live, staged, or in progress. This page introduces all three and explains how they work together. Implementation details, configuration, and client library usage are covered in the linked sub-pages. --- ## Content API The content API is the primary way to consume SleekCMS content in your own frontend. It delivers your entire site's content — pages, entries, images, options, and configuration — as structured JSON, accessible via a single authenticated endpoint. ### How It Works You authenticate with a site token and request content from a specific environment. The API returns a JSON payload containing all of your site's published content in one response: ``` GET https://pub.sleekcms.com/{site-id}/{environment} Authorization: {site-token} ``` The response is a single payload with this top-level structure: ```json { "pages": [ ... ], "entries": { ... }, "images": { ... }, "options": { ... }, "config": { ... }, "_tag": "..." } ``` **`pages`** — An array of all page records across all page models. Each page object contains its field data, a `_path` property for routing, and a `_slug` for collection pages. **`entries`** — An object keyed by entry handle. Single entries resolve to an object, entry collections resolve to an array of objects. Referenced entries are also resolved inline within page data. **`images`** — Named images registered at the site level, accessible by key. **`options`** — Option sets accessible by handle, each containing an array of label-value pairs. **`config`** — Site-level configuration including the site title, origin URL, and subdirectory setting. **`_tag`** — A version identifier for the content snapshot. This changes whenever content is published to the requested environment. ### Client Libraries While you can call the API directly with any HTTP client, SleekCMS provides official client libraries that handle authentication, caching, environment resolution, and typed access to your content. **`@sleekcms/client`** — The core JavaScript/TypeScript client. Available via npm or CDN. Offers two client modes: a sync client that prefetches all content (ideal for static site generation) and an async client that fetches on demand (ideal for server-side rendering). ```typescript import { createSyncClient } from '@sleekcms/client'; const client = await createSyncClient({ siteToken: 'your-site-token', env: 'latest' }); const page = client.getPage('/about'); const posts = client.getPages('/blog'); const footer = client.getEntry('footer'); const slugs = client.getSlugs('/blog'); ``` **`@sleekcms/react`** — React hooks for client-side content fetching. Wraps the async client with loading states, error handling, and automatic refetching — designed for React SPAs and client components. ```tsx import { SleekCMSProvider, usePage } from '@sleekcms/react'; function App() { return ( <SleekCMSProvider siteToken="your-site-token"> <AboutPage /> </SleekCMSProvider> ); } function AboutPage() { const { data, loading } = usePage('/about'); if (loading) return <p>Loading...</p>; return <h1>{data?.title}</h1>; } ``` Both libraries support environment selection, language targeting for internationalized content, and custom cache adapters for controlling how content is stored between requests. → [Content API](/publish/api) → [@sleekcms/client](/publish/api/client) → [@sleekcms/react](/publish/api/react) --- ## Publish Triggers Publish triggers are automated actions that fire when you publish content. They let you connect SleekCMS to your external build and deployment pipelines — so that when content changes, your website rebuilds, your CDN cache invalidates, or any downstream system gets notified. ### What Triggers Do A trigger sends an HTTP request to a URL you configure. This can be a webhook endpoint, a build hook, or any URL that accepts an HTTP call. When triggered, it tells the receiving system that content has changed and action is needed. ### Common Use Cases **GitHub Actions** — Trigger a GitHub Actions workflow to rebuild your static site when content is published. Your workflow pulls fresh content from the API, runs your build process, and deploys the result. **Netlify deploys** — Call a Netlify build hook to trigger a site rebuild. Netlify pulls your repository, builds the site with the latest content, and deploys it to their CDN — all initiated by the publish trigger. **Custom webhooks** — Call any HTTP endpoint. This could be a Slack notification, a cache purge on your CDN, an indexing request for your search service, or any system that needs to know when content changes. ### How Triggers Fit the Workflow Triggers bridge the gap between content management and content delivery. The typical flow is: ``` Editor publishes content │ ▼ SleekCMS fires publish trigger(s) │ ▼ External system receives webhook │ ▼ Site rebuilds / cache purges / action executes ``` You can configure multiple triggers on a single site. Each trigger fires independently, so a single publish action can simultaneously kick off a Netlify deploy, notify a Slack channel, and purge a CDN cache. Trigger access can be restricted per editor using fine-grained access controls. This means you can allow senior editors to fire production build triggers while preventing other editors from triggering deploys. → [Automating using Triggers](/publish/triggers) → [Customizable Access](/settings/members/access-control) --- ## Environments Environments are how you manage content versions in SleekCMS. An environment is a named alias that points to a specific snapshot of your content. When you access content through the API, you request it from an environment — and the environment determines which version of the content you receive. ### The `latest` Environment Every site has a built-in `latest` environment that always points to the current state of your content. When you create or edit content and publish, `latest` reflects those changes immediately. Most integrations start with `latest` and it's the default when no environment is specified. ```typescript // These are equivalent — both fetch the current content const client = await createSyncClient({ siteToken: 'your-site-token', env: 'latest' }); const client = await createSyncClient({ siteToken: 'your-site-token' }); ``` ### Named Environments Beyond `latest`, you can create named environments that point to specific content snapshots. A named environment captures the state of your content at a point in time and holds it there until you explicitly update it. This is how you decouple content editing from content delivery. Editors work on content freely — adding pages, editing entries, reordering blocks — and `latest` reflects every change in real time. But your production site can point to a named environment like `production` that only updates when an admin explicitly promotes content to it. ### Staging and Production Workflows Named environments enable multi-stage publishing workflows. A common setup: **`latest`** — The working state. Every edit is reflected here immediately. Use this for internal previews and content review. **`staging`** — A promotion target. When content is ready for QA or stakeholder review, promote `latest` to `staging`. Your staging site pulls from this environment, showing exactly what will go live. **`production`** — The live content. When staging is approved, promote it to `production`. Your production site pulls from this environment, and nothing changes on the live site until this promotion happens. Each environment is an independent snapshot. Editing content after promoting to `staging` changes `latest` but does not affect `staging` until the next promotion. This gives content teams a safe space to work without risking the live site. ### Environments in the API and Client The environment is specified when creating a client or making an API request. The API URL includes the environment name, and the client libraries accept it as a configuration option: ```typescript // Fetch content from the production environment const client = await createSyncClient({ siteToken: 'your-site-token', env: 'production' }); ``` ``` GET https://pub.sleekcms.com/{site-id}/production Authorization: {site-token} ``` The `resolveEnv` option on the client resolves an environment alias to its underlying version tag. This is useful for CDN cache invalidation — when the environment points to a new snapshot, the resolved tag changes, producing a different cache key and ensuring fresh content is served. ```typescript const client = createAsyncClient({ siteToken: 'your-site-token', env: 'production', resolveEnv: true // Resolves to version tag for cache busting }); ``` → [Environments & Content Versions](/publish/envs) --- ## How the Three Work Together The API, triggers, and environments form a complete content delivery pipeline. Here's how they connect in a typical production setup: **Content is created and edited** in SleekCMS. Every change is immediately available through the `latest` environment for preview and internal review. **When content is ready**, an admin promotes it to a named environment like `production`. The content snapshot at that moment becomes the production content. **The promotion fires publish triggers** — a Netlify build hook, a GitHub Actions workflow, or any configured webhook. The external system receives the notification and initiates a rebuild. **The build system fetches content** from the `production` environment using the content API (via `@sleekcms/client` or a direct HTTP call). It generates the site with the promoted content and deploys the result. The end-to-end flow: ``` Edit content → Promote to environment → Trigger fires → Build fetches from API → Site deploys ``` This pipeline works with any frontend framework, any hosting provider, and any build system. SleekCMS handles the content management and notification layer. Your existing tools handle the build and deployment. --- ## What's Next - [Content API](/publish/api) — Endpoint details, authentication, and response structure. - [@sleekcms/client](/publish/api/client) — The official JavaScript/TypeScript client library. - [@sleekcms/react](/publish/api/react) — React hooks for client-side content fetching. - [Automating using Triggers](/publish/triggers) — Configuring webhooks, build hooks, and automated workflows. - [Environments & Content Versions](/publish/envs) — Creating environments, promoting content, and managing snapshots. - [Site Builder](/builder) — The alternative to API integration — generating static sites directly within SleekCMS. The content API is how you deliver SleekCMS content to your website, app, or frontend. It exposes your entire site's published content — pages, entries, images, option sets, and configuration — as structured JSON through a single authenticated endpoint. You fetch content by environment, and the response includes everything your frontend needs to render the site. This page covers the API endpoint, authentication, response structure, the official client library and its methods, caching, internationalization, and framework integration patterns. --- ## Endpoint and Authentication The content API uses a single endpoint per site. You authenticate with a site token passed as the `Authorization` header and specify which environment to fetch from in the URL path. ``` GET https://pub.sleekcms.com/{site-id}/{environment} ``` **`{site-id}`** — Your site's unique identifier, assigned when you create the site in SleekCMS. **`{environment}`** — The environment alias to fetch content from. Use `latest` for the current working state, or a named environment like `production` or `staging` for a specific content snapshot. ### Authentication Every request requires a site token in the `Authorization` header. You can find your site token in the SleekCMS admin interface under site settings. ```bash curl -H "Authorization: YOUR_SITE_TOKEN" \ "https://pub.sleekcms.com/YOUR_SITE_ID/latest" ``` The site token is a read-only credential — it grants access to published content but cannot modify content, models, or settings. Treat it as a secret in server-side environments but be aware that it is necessarily exposed in client-side applications that call the API directly from the browser. → [Environments & Content Versions](/publish/envs) --- ## Response Structure The API returns your entire site's content as a single JSON payload. This full-payload approach means your frontend receives everything in one request — no pagination, no follow-up queries, no resolving references. ```json { "pages": [ ... ], "entries": { ... }, "images": { ... }, "videos": { ... }, "options": { ... }, "config": { ... }, "_tag": "..." } ``` ### `pages` An array of all page records across all page models. Each page object contains its field data plus system properties: **`_path`** — The page's URL path. Static pages have a fixed path like `/about`. Collection pages have a path that includes the slug, like `/blog/hello-world`. **`_slug`** — Present on collection pages only. The slug portion of the path, used for route generation. **`_meta`** — Metadata including `key` (the internal record identifier), `created_at`, and `updated_at` timestamps. All content fields — text, images, references, groups, collections, dynamic blocks — are resolved inline. Referenced entries are embedded as full objects, not IDs. Markdown and rich text fields are delivered as rendered HTML. Image fields are delivered as objects with `url`, `raw`, `alt`, and variant properties. ```json { "_slug": "italian", "_path": "/blog/italian", "title": "Italian inspiration on a budget", "published_date": "2025-03-03", "image": { "alt": "Fresh Italian ingredients on a rustic table", "url": "https://img.sleekcms.com/az41/m7urgy6z.webp", "raw": "https://img.sleekcms.com/az41/m7urgy6z", "source": null, "light": null, "dark": null }, "category": ["veg", "vegan"], "content": "<p>Create a blog post subtitle...</p>", "_meta": { "key": "cms:r2s2t1", "updated_at": "2026-01-11T23:32:48.894Z", "created_at": "2026-01-11T23:32:48.894Z" } } ``` ### `entries` An object keyed by entry handle. Single entries (like a footer or site settings) resolve to an object. Entry collections (like authors or categories) resolve to an array of objects. ```json { "entries": { "footer": { "copyright_text": "© 2025 Salt & Pepper. All rights reserved.", "socials": [ { "icon": "fab fa-facebook-f", "link": "/" }, { "icon": "fab fa-instagram", "link": "/" } ] } } } ``` When a page references an entry through a reference field, the entry's full data is embedded inline in the page object. You don't need to look up entries separately — they're already resolved in the page data. ### `images` Named images registered at the site level, keyed by name. Each image object follows the same structure as image fields on content records: `url`, `raw`, `alt`, `source`, and optional `light`/`dark` theme variants. ### `options` Option sets with assigned handles, keyed by handle name. Each option set is an array of `{ label, value }` pairs. ### `config` Site-level configuration: **`title`** — The site title, if set. **`origin`** — The origin URL for the site, used for generating absolute URLs. **`subdirectory`** — The subdirectory path, if the site is deployed under a subpath. ### `_tag` A version tag for the content snapshot. This value changes whenever content is published to the requested environment. Use it for cache invalidation — if the tag hasn't changed, the content hasn't changed. --- ## Client Library The official `@sleekcms/client` library wraps the API with typed methods for querying pages, entries, images, and option sets. It handles authentication, caching, environment resolution, and provides both synchronous and asynchronous client modes. ### Installation ```bash npm install @sleekcms/client ``` Or include directly from a CDN for browser usage: ```html <script src="https://unpkg.com/@sleekcms/client"></script> ``` When loaded from a CDN, the library is available globally as `SleekCMS`. ### Creating a Client The library provides two client constructors. Both accept the same configuration options but differ in how they fetch and return content. **`createSyncClient()`** — Fetches all content upfront during initialization. After initialization, all methods return data synchronously. Best for static site generation where you want instant access to the full content at build time. ```typescript import { createSyncClient } from '@sleekcms/client'; const client = await createSyncClient({ siteToken: 'your-site-token', env: 'latest' }); // All methods return data immediately — no await needed const page = client.getPage('/about'); const posts = client.getPages('/blog'); ``` **`createAsyncClient()`** — Fetches content on demand. Every method returns a Promise. Best for server-side rendering where you want fresh content per request without loading the entire payload upfront. ```typescript import { createAsyncClient } from '@sleekcms/client'; const client = createAsyncClient({ siteToken: 'your-site-token', resolveEnv: true }); // All methods are async const page = await client.getPage('/pricing'); const posts = await client.getPages('/blog'); ``` ### Configuration Options | Option | Type | Default | Description | |---|---|---|---| | `siteToken` | `string` | required | Your site token from SleekCMS. | | `env` | `string` | `'latest'` | Environment alias to fetch content from. | | `resolveEnv` | `boolean` | `false` | Resolves the environment alias to its underlying version tag. Useful for CDN cache invalidation — produces a different cache key when content changes. Adds some latency to each request. | | `lang` | `string` | — | Language code for internationalized content. When set, the client returns translations for the specified locale. | | `cache` | `SyncCacheAdapter \| AsyncCacheAdapter` | In-memory | Custom cache adapter for storing fetched content. | | `cacheMinutes` | `number` | — | Cache expiration time in minutes. If not set, cached content never expires. | --- ## Client Methods All methods are available on both the sync and async clients. On the sync client, they return values directly. On the async client, they return Promises. ### `getContent(search?)` Returns the full content payload, or a subset filtered using a [JMESPath](https://jmespath.org/) query. This is the most flexible method — JMESPath expressions can filter, project, and transform the content data. ```typescript // Full content payload const content = client.getContent(); // All pages const pages = client.getContent('pages'); // A specific entry const footer = client.getContent('entries.footer'); // Site title from config const title = client.getContent('config.title'); // Filter pages with JMESPath const aboutPage = client.getContent('pages[?_path==`/about`] | [0]'); const featuredPosts = client.getContent('pages[?featured==`true`]'); ``` JMESPath is a query language for JSON. It supports filtering (`[?field==\`value\`]`), projections, slicing, and multi-select expressions. For full syntax, see [jmespath.org](https://jmespath.org/). ### `getPage(path)` Returns a single page by exact path match. Returns `null` if no page exists at the given path. ```typescript const about = client.getPage('/about'); const post = client.getPage('/blog/hello-world'); const home = client.getPage('/'); ``` The path must match exactly. `/about` matches the about page but not `/about/team`. For prefix matching, use `getPages()`. ### `getPages(path, options?)` Returns all pages whose path starts with the given prefix. Use this to retrieve all pages within a section of your site. ```typescript const posts = client.getPages('/blog'); const products = client.getPages('/shop/products'); ``` **Options:** | Option | Type | Description | |---|---|---| | `collection` | `boolean` | When `true`, only returns pages that belong to a collection (pages with a `_slug`). Excludes static index pages. | ```typescript // All pages under /blog, including the /blog index page const allBlogPages = client.getPages('/blog'); // Only blog post collection items (excludes the /blog index) const posts = client.getPages('/blog', { collection: true }); ``` ### `getEntry(handle)` Returns an entry by its handle. Single entries return an object. Entry collections return an array of objects. ```typescript // Single entry (e.g., admin-only site settings) const header = client.getEntry('header'); // { logo: { url: '...' }, links: [...] } // Entry collection const team = client.getEntry('team-members'); // [{ name: 'Jane', role: 'Engineer', ... }, { name: 'Alex', role: 'Designer', ... }] ``` ### `getSlugs(path)` Extracts slug values from all collection pages under a given path. Returns an array of strings. This is the primary method for generating static routes in frameworks like Next.js, Astro, and 11ty. ```typescript const slugs = client.getSlugs('/blog'); // ['hello-world', 'nextjs-tips', 'typescript-guide'] const productSlugs = client.getSlugs('/products'); // ['basic-plan', 'pro-plan', 'enterprise'] ``` ### `getImage(name)` Returns a named image registered at the site level. ```typescript const logo = client.getImage('logo'); // { url: 'https://...', alt: 'Company logo', raw: '...', ... } ``` ### `getOptions(name)` Returns an option set by handle as an array of label-value pairs. ```typescript const categories = client.getOptions('categories'); // [{ label: 'Technology', value: 'tech' }, { label: 'Design', value: 'design' }, ...] ``` This is useful for building filter interfaces, navigation menus, or client-side dropdowns that need the full list of available options rather than just the selected value on a content record. --- ## Caching The client includes built-in caching to reduce API calls and improve performance. By default, fetched content is stored in an in-memory cache. You can provide your own cache adapter and control expiration. ### Default Behavior With no cache configuration, the client uses an in-memory cache that persists for the lifetime of the process. Content is fetched once and served from memory on subsequent calls. ### Custom Cache Adapters Any object that implements `getItem` and `setItem` works as a cache adapter. The browser's `localStorage` is a natural fit for client-side applications: ```typescript const client = await createSyncClient({ siteToken: 'your-site-token', cache: localStorage, cacheMinutes: 60 * 24 // Cache expires after 1 day }); ``` For custom storage backends, implement the adapter interface: ```typescript // Synchronous adapter interface SyncCacheAdapter { getItem(key: string): string | null; setItem(key: string, value: string): void; } // Asynchronous adapter (for IndexedDB, Redis, remote caches) interface AsyncCacheAdapter { getItem(key: string): Promise<string | null>; setItem(key: string, value: string): Promise<void>; } ``` ### Cache Expiration The `cacheMinutes` option controls how long cached content remains valid. When the cache expires, the next request fetches fresh content from the API and repopulates the cache. If `cacheMinutes` is not set, cached content never expires — it persists until the cache is manually cleared or the process restarts (for in-memory caches). --- ## Internationalization The client supports multi-language content through the `lang` configuration option. When set, the API returns translated content for the specified locale. ```typescript const client = await createSyncClient({ siteToken: 'your-site-token', lang: 'es' }); const page = client.getPage('/about'); // Returns the Spanish translation of the about page ``` Language codes correspond to the locales configured in your SleekCMS site settings. If a translation doesn't exist for the requested locale, the API falls back to the default language content. To serve multiple languages, create a separate client instance per locale: ```typescript const clients = { en: await createSyncClient({ siteToken: 'your-site-token', lang: 'en' }), es: await createSyncClient({ siteToken: 'your-site-token', lang: 'es' }), de: await createSyncClient({ siteToken: 'your-site-token', lang: 'de' }), }; const page = clients[userLocale].getPage('/about'); ``` → [Supporting Multiple Languages](/settings/config/translate) --- ## Content Data Shapes Understanding the shape of content objects in API responses helps you build typed frontends and render content correctly. ### Page Objects Every page object includes its content fields plus system properties: | Property | Type | Description | |---|---|---| | `_path` | `string` | The full URL path for the page. | | `_slug` | `string` | The slug segment (collection pages only). | | `_meta.key` | `string` | Internal record identifier. | | `_meta.created_at` | `string` | ISO 8601 creation timestamp. | | `_meta.updated_at` | `string` | ISO 8601 last update timestamp. | All other properties are the page's content fields, named by their handle. ### Image Objects Image fields and named images share the same object structure: | Property | Type | Description | |---|---|---| | `url` | `string` | The optimized image URL (typically WebP format). | | `raw` | `string` | The base image URL without format conversion. Used for building transformed URLs with query parameters. | | `alt` | `string \| null` | Alt text for the image. | | `source` | `string \| null` | The image source provider (e.g., `"unsplash"`, `null` for uploaded images). | | `light` | `object \| null` | Light theme variant of the image, if set. | | `dark` | `object \| null` | Dark theme variant of the image, if set. | ### Entry Objects Entry objects contain their content fields plus a `_meta` block. The shape depends on the entry model's field configuration. When referenced from a page, the full entry object is embedded inline. ### Dynamic Block Arrays Dynamic block fields return an array of block objects. Each block includes a `_type` property identifying the block model handle, plus the block's content fields: ```typescript const page = client.getPage('/'); // page.sections → [ // { _type: 'hero', heading: 'Welcome', image: { url: '...' } }, // { _type: 'features', items: [...] }, // { _type: 'cta', heading: 'Get Started', buttonLabel: 'Sign Up' } // ] ``` --- ## TypeScript The client library is fully typed. Import the type definitions to annotate your code: ```typescript import type { SleekClient, SleekAsyncClient, Page, Entry, Image, Options } from '@sleekcms/client'; ``` --- ## Framework Integration The client works with any JavaScript framework. The choice between sync and async clients maps to the rendering strategy your framework uses. ### Next.js (Static Site Generation) Use the sync client in `generateStaticParams` and page components to fetch content at build time: ```typescript // app/blog/[slug]/page.tsx import { createSyncClient } from '@sleekcms/client'; export async function generateStaticParams() { const client = await createSyncClient({ siteToken: process.env.SLEEKCMS_SITE_TOKEN! }); return client.getSlugs('/blog').map((slug) => ({ slug })); } export default async function Post({ params }: { params: { slug: string } }) { const client = await createSyncClient({ siteToken: process.env.SLEEKCMS_SITE_TOKEN! }); const post = client.getPage(`/blog/${params.slug}`); return <h1>{post?.title}</h1>; } ``` ### Next.js (Server-Side Rendering) Use the async client with `resolveEnv` to ensure fresh content on each request: ```typescript // app/blog/page.tsx import { createAsyncClient } from '@sleekcms/client'; const client = createAsyncClient({ siteToken: process.env.SLEEKCMS_SITE_TOKEN!, resolveEnv: true }); export default async function BlogPage() { const posts = await client.getPages('/blog'); return ( <div> {posts?.map((post) => ( <article key={post._path}> <h2>{post.title}</h2> </article> ))} </div> ); } ``` ### SvelteKit ```typescript // +page.server.ts import { createAsyncClient } from '@sleekcms/client'; const client = createAsyncClient({ siteToken: process.env.SLEEKCMS_SITE_TOKEN, resolveEnv: true }); export async function load() { const posts = await client.getPages('/blog'); return { posts }; } ``` ### Astro ```astro --- // src/pages/blog/index.astro import { createSyncClient } from '@sleekcms/client'; const client = await createSyncClient({ siteToken: import.meta.env.SLEEKCMS_SITE_TOKEN }); const posts = client.getPages('/blog'); --- <div> {posts?.map((post) => ( <article> <h2>{post.title}</h2> </article> ))} </div> ``` ### React (Client-Side) For React SPAs, use the `@sleekcms/react` package which provides hooks with built-in loading states and error handling: ```tsx import { SleekCMSProvider, usePage, usePages, useEntry } from '@sleekcms/react'; function App() { return ( <SleekCMSProvider siteToken="your-site-token"> <BlogPage /> </SleekCMSProvider> ); } function BlogPage() { const { data: posts, loading } = usePages('/blog'); if (loading) return <p>Loading...</p>; return ( <div> {posts?.map((post) => ( <article key={post._path}> <h2>{post.title}</h2> </article> ))} </div> ); } ``` → [@sleekcms/react](/publish/api/react) --- ## What's Next - [@sleekcms/client](/publish/api/client) — Full client library reference including CDN usage and advanced configuration. - [@sleekcms/react](/publish/api/react) — React hooks for client-side content fetching. - [Environments & Content Versions](/publish/envs) — Managing content snapshots and multi-stage publishing. - [Automating using Triggers](/publish/triggers) — Triggering builds and workflows when content is published. - [Content Modeling](/model) — How content models define the shape of API responses. - [Content Field Types](/model/fields) — How each field type appears in API responses. Publish triggers connect SleekCMS to your external build and deployment systems. When you publish content, triggers fire automatically — calling the APIs of services like GitHub, Netlify, or any custom webhook endpoint. This is how content changes flow from SleekCMS into your live website without manual intervention. A trigger has two parts: a **target** that defines where the call goes and how to authenticate, and the **trigger action** that fires when content is published. You configure targets once, and SleekCMS calls them each time you publish. This page covers how triggers work, the three supported target types, authentication and security, and how triggers fit into your publishing workflow. --- ## How Triggers Work The mechanism is straightforward. You configure one or more targets on your site — each pointing to an external service. When you publish content, you choose which environment to publish to and which target to trigger. SleekCMS calls the selected target's API, and the external service performs its action: a GitHub Actions workflow runs, a Netlify build starts, or a webhook endpoint processes the notification. Publishing is a deliberate, selective action. You pick the environment, you pick the target. SleekCMS does not fire all targets automatically — you control exactly which external system gets notified on each publish. This means you can publish to staging and trigger only a staging webhook, then later publish to production and trigger a different target. Triggers fire on publish actions only. Saving a draft or editing content does not fire triggers. Your external systems only rebuild when you intentionally publish and select a target. --- ## Target Types SleekCMS supports three target types. Each integrates with a different service and uses that service's native API for triggering actions. ### GitHub Actions A GitHub Actions target triggers a workflow in a GitHub repository. When fired, SleekCMS calls the GitHub API to dispatch a workflow event, which starts the specified workflow run. This is the most common setup for static site generation. Your GitHub repository contains your frontend code (Next.js, Astro, 11ty, or similar), and the workflow pulls fresh content from the SleekCMS content API, builds the site, and deploys the result. **Configuration requires:** - **Repository** — The GitHub repository that contains the workflow (e.g., `your-org/your-site`). - **Workflow** — The workflow file to trigger (e.g., `build.yml`). - **Personal access token** — A GitHub personal access token with permission to trigger workflow dispatches on the repository. When triggered, SleekCMS sends a `workflow_dispatch` event to the GitHub API. Your workflow file must be configured to listen for this event: ```yaml # .github/workflows/build.yml name: Build and Deploy on: workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run build env: SLEEKCMS_SITE_TOKEN: ${{ secrets.SLEEKCMS_SITE_TOKEN }} - name: Deploy # Your deployment step ``` The workflow has full access to your repository and secrets, so your build script can fetch content from the SleekCMS API using your site token stored as a GitHub secret. ### Netlify A Netlify target triggers a site build on Netlify. When fired, SleekCMS calls a Netlify build hook URL, which starts a fresh build and deployment of your Netlify site. This is the simplest setup if your site is already hosted on Netlify. Netlify build hooks are URLs that trigger a build when called — no authentication token management required beyond the hook URL itself. **Configuration requires:** - **Build hook URL** — The Netlify build hook URL for your site. You generate this in your Netlify site settings under **Build & deploy → Build hooks**. The build hook URL acts as both the endpoint and the credential — anyone with the URL can trigger a build. SleekCMS stores it encrypted. When triggered, Netlify pulls your repository, runs your build command with the latest content, and deploys the result to its CDN. If your build script fetches from the SleekCMS content API, the rebuild automatically picks up the newly published content. ### Custom Webhook A custom webhook target calls any HTTP endpoint you specify. This is the flexible option — it works with any service that accepts incoming HTTP requests, from Slack and Discord to custom serverless functions and third-party automation platforms. **Configuration requires:** - **URL** — The endpoint to call when the trigger fires. - **Secret** (optional) — An arbitrary secret string that SleekCMS sends with the request as the `Authorization` header. The receiving endpoint can verify this value to authenticate that the request came from SleekCMS. When triggered, SleekCMS sends an HTTP request to the configured URL. If a secret is configured, it is included as the `Authorization` header value, allowing the receiving service to verify the request origin. **Common webhook uses:** - **CDN cache purge** — Call your CDN's purge API to invalidate cached pages after content changes. - **Search re-indexing** — Notify your search service (Algolia, Meilisearch, Elasticsearch) to re-crawl and index updated content. - **Team notifications** — Post to a Slack or Discord channel when content is published, keeping the team informed of changes. - **Custom automation** — Call a serverless function that performs any post-publish logic specific to your workflow. --- ## Security Trigger targets store sensitive credentials — GitHub personal access tokens, Netlify build hook URLs, and webhook secrets. SleekCMS encrypts all target credentials at rest using a site-specific salt. The credentials are decrypted only at the moment a trigger fires and are never exposed in the admin interface after initial configuration. ### GitHub Tokens GitHub personal access tokens are encrypted at rest. The token is used to authenticate the workflow dispatch API call and is never visible after you save the target configuration. If you need to rotate the token, update the target with a new one. ### Netlify Build Hooks Netlify build hook URLs contain an embedded secret and are treated as credentials. They are encrypted at rest alongside other target secrets. ### Webhook Secrets For custom webhooks, you can specify an arbitrary secret string. SleekCMS sends this value as the `Authorization` header with every request to the webhook endpoint. Your endpoint should validate this header to ensure the request originated from SleekCMS and reject requests with missing or incorrect values. ``` # What the receiving endpoint sees Authorization: your-configured-secret ``` The webhook secret is encrypted at rest using the same site-specific salt as other credentials. Choose a strong, unique secret for each webhook target. --- ## Multiple Targets A site can have multiple targets configured simultaneously. When publishing, you select which target to trigger for that specific publish action. You are not required to trigger all targets every time — you choose the one appropriate for the situation. A typical multi-target setup might include: - A **GitHub Actions** target for rebuilding and deploying the production site. - A **Netlify** target for deploying a staging preview. - A **custom webhook** that posts a notification to a Slack channel. Different team members might use different targets depending on the context. A content editor might trigger the staging Netlify target for review, while an admin triggers the GitHub Actions target for production deployment. Access controls determine which members can fire which targets. --- ## Access Control Trigger access can be restricted per editor using fine-grained access controls. By default, only admins can trigger targets. When fine-grained access is enabled for an editor, you can grant them permission to trigger specific targets — allowing a senior editor to trigger staging deploys while restricting other editors from initiating production builds. This is configured in the site members settings, not in the target configuration itself. The targets are shared across the site; the access controls determine which members can select which targets when publishing. → [Site Members](/settings/members) → [Customizable Access](/settings/members/access-control) --- ## Triggers and Environments When publishing, you select both an environment and a target. The environment determines which content snapshot gets updated. The target determines which external system gets notified. These are independent choices made at publish time. A typical workflow: 1. Content editors create and edit content. The `latest` environment reflects all changes in real time. 2. When content is ready, a user publishes to a named environment (e.g., `production`) and selects a target to trigger (e.g., the GitHub Actions target). 3. The selected target fires — calling the GitHub API, Netlify build hook, or webhook endpoint. 4. The external system receives the call and performs its action. It's important to understand that the environment you publish to and the environment your scripts fetch from are independent. Publishing to `production` and triggering a GitHub Actions workflow doesn't force the workflow to fetch from `production` — your build script decides which environment to request from the content API. This gives you flexibility: a staging build triggered after a production publish could still fetch from `staging` for comparison, or a single workflow could fetch from multiple environments. The environment published to is the content gate. The target triggered is the automation gate. Your build scripts control the rest. → [Environments & Content Versions](/publish/envs) --- ## What's Next - [Publishing Content](/publish) — Overview of the content delivery pipeline. - [Content API](/publish/api) — The API that your build systems call to fetch content after a trigger fires. - [Environments & Content Versions](/publish/envs) — Managing content snapshots and the promotion workflow that fires triggers. - [Site Members](/settings/members) — Configuring who can fire triggers. - [Deploying Sites](/builder/deploy) — Alternative deployment through the integrated site builder, without triggers. Environments in SleekCMS are labels — named aliases that point to a version of your content. When you publish content to an environment, the alias updates to point to the current content snapshot. When your frontend fetches content from that environment, it gets the snapshot the alias points to. This is how you control which version of your content is live. You edit freely, and nothing changes on your production site until you explicitly publish to the production environment. This page covers how environments work, how aliases resolve to content versions, CDN caching behavior, and how to configure your client to get fresh content after a publish. --- ## How Environments Work An environment is a label you create — `production`, `staging`, `qa`, or any name that fits your workflow. Each label is an alias that points to a specific content version. When you publish content to an environment, the alias updates to point to the latest content snapshot at that moment. The content itself isn't duplicated. Publishing to an environment doesn't create a copy — it moves the alias pointer. The `production` label now resolves to the content as it exists right now. If you publish again later after making edits, the alias moves again to the new snapshot. ### The `latest` Environment Every site has a built-in `latest` environment. This is not a label you create — it always exists and always points to the current state of your content. Every edit, every save, every change is immediately reflected in `latest`. `latest` is useful for previewing content during editing, for internal review, and for development. It is not typically used for production, because it updates on every change — including half-finished edits. ### Named Environments Named environments are the labels you create for controlled publishing. You might have one (`production`) or several (`staging`, `production`, `qa`). Each is an independent alias that only moves when you explicitly publish to it. The simplest setup is a single `production` environment. You edit content, review it via `latest`, and when it's ready, publish to `production`. Your live site reads from `production` and only sees the content you've promoted. A multi-environment setup adds stages: **`latest`** — Every edit is reflected here immediately. Use for in-progress work and real-time previews. **`staging`** — Publish here when content is ready for review. A staging site reads from this environment, showing stakeholders exactly what will go live. **`production`** — Publish here when staging is approved. Your live site reads from this environment. Each environment is just a label pointing to a snapshot. You can have as many as your workflow requires. --- ## Publishing to an Environment Publishing is a deliberate action. You select an environment from your list of labels, and SleekCMS updates that alias to point to the current content. Optionally, you also select a trigger target to notify an external system. After publishing, the environment's alias resolves to the new content version. Any frontend fetching from that environment gets the updated content on its next request (subject to caching — see below). Publishing to one environment does not affect others. You can publish to `staging` without changing what `production` points to. Each alias moves independently. --- ## Fetching Content by Environment Your frontend specifies which environment to fetch from when creating a client or calling the API. The environment name appears in the API URL path: ``` GET https://pub.sleekcms.com/{site-id}/production GET https://pub.sleekcms.com/{site-id}/staging GET https://pub.sleekcms.com/{site-id}/latest ``` With the client library: ```typescript // Fetch from the production environment const client = await createSyncClient({ siteToken: 'your-site-token', env: 'production' }); // Fetch from staging const stagingClient = await createSyncClient({ siteToken: 'your-site-token', env: 'staging' }); ``` If `env` is not specified, the client defaults to `latest`. → [Content API](/publish/api) --- ## CDN Caching and `resolveEnv` Content served through the API is cached on a CDN for fast delivery. The cache key is based on the environment name. This means that after you publish to an environment — updating the alias to a new content version — the CDN may still serve the previous cached version until the cache expires. The `resolveEnv` option on the client solves this. When enabled, the client resolves the environment alias to its underlying version tag before fetching content. Because the version tag changes every time you publish to that environment, the resolved URL is different, producing a new cache key that bypasses the stale cache. ```typescript const client = createAsyncClient({ siteToken: 'your-site-token', env: 'production', resolveEnv: true }); ``` **Without `resolveEnv`** — The client fetches from `/production`, which may return a cached response from the CDN even after you've published new content. The cache will eventually expire and refresh, but there's a delay. **With `resolveEnv`** — The client first resolves `production` to its current version tag (e.g., `mlciujgn`), then fetches from that tag. After a publish, the tag changes, producing a different URL that the CDN hasn't cached yet. The result is fresh content immediately after publishing. The tradeoff is latency. With `resolveEnv` enabled, every request makes an extra call to resolve the alias before fetching content. For server-side rendering where freshness matters on every request, this is usually worth it. For static site generation where you build once, the extra call happens only at build time and is negligible. ```typescript // SSR — use resolveEnv for fresh content on every request const client = createAsyncClient({ siteToken: 'your-site-token', env: 'production', resolveEnv: true }); // SSG — resolveEnv is optional, runs once at build time const client = await createSyncClient({ siteToken: 'your-site-token', env: 'production' }); ``` → [@sleekcms/client](/publish/api/client) --- ## Common Patterns ### Single Environment (Simple) One `production` environment. Edit content, review via `latest`, publish to `production` when ready. Your live site points to `production`. This works well for small teams and sites where content goes through minimal review before going live. ### Staging and Production Two environments. Publish to `staging` for review, then publish to `production` when approved. A staging site reads from `staging`, the live site reads from `production`. This adds a review gate without adding complexity. Stakeholders preview on staging, approve, and then the same content is promoted to production. ### Per-Feature or Per-Campaign Environments Create environments for specific purposes — `campaign-launch`, `redesign`, `holiday-content`. Publish to them when preparing content for specific initiatives, and point preview URLs at each one for review. When the initiative goes live, publish to `production`. These environments are just labels. Create them when you need them, and they cost nothing to maintain. The content isn't duplicated — only the alias pointer exists. --- ## What's Next - [Content API](/publish/api) — Endpoint details, response structure, and client library usage. - [Automating using Triggers](/publish/triggers) — Triggering builds and workflows when publishing to environments. - [Publishing Content](/publish) — Overview of the full content delivery pipeline. - [Site Configuration](/settings/config) — Site-level settings and configuration. The site configuration page contains settings that affect how your entire content site behaves — from image processing and localization to key naming conventions and site identity. These are site-level controls managed by administrators, accessible from the settings area of the SleekCMS interface. This page is a reference for every setting on the configuration screen. --- ## Site Builder (SSG) Enables the integrated static site builder. When active, you can bind EJS templates to your content models and generate a fully deployable static website from within SleekCMS — no external build tools, servers, or repositories required. With the site builder enabled, the template editor, preview system, and deployment options become available in the interface. Disabling it hides these features and treats the site as a purely headless CMS where content is consumed exclusively through the content API. → [Site Builder](/builder) --- ## Page Type Models Controls whether page models use the "page type" classification — models that behave as entries with a slug field. This affects how content is organized in the editor and how routes are generated. When enabled, page type models appear alongside standard page models in the modeling interface. This is primarily relevant if you are using the site builder or if your content architecture benefits from treating certain routable content as entry-like structures with slug-based routing. → [Page Models](/model/pages) --- ## Image Processing with Imgix Enables Imgix as the image processing backend for your site. When active, image URLs are routed through Imgix for on-the-fly transformations — resizing, cropping, format conversion, and optimization — instead of using SleekCMS's default image processing. ### Web Folder Source Base URL When using Imgix, you configure a **web folder source base URL**. This is the base URL that Imgix uses to fetch your original images. Imgix retrieves the source image from this URL and applies transformations based on query parameters before delivering the optimized result. To set this up, you need an Imgix account with a web folder source configured to point to your image origin. The base URL you enter here must match the source configuration in your Imgix dashboard. → [Image Processing using Imgix](/settings/config/imgix) --- ## Localization and Translations Enables multi-language content support for your site. When active, you can maintain translated versions of your content across the locales you configure. Supported locales and languages are configured separately from the main menu. This setting activates the translation workflow — once enabled, content editors see language-switching controls in the editor and can create and manage translated content for each configured locale. The content API respects the `lang` parameter when fetching content, returning the appropriate translation for the requested locale. In the site builder, templates have access to the current language context during rendering. ```typescript // Fetch content in a specific language const client = await createSyncClient({ siteToken: 'your-site-token', lang: 'es' }); const page = client.getPage('/about'); // Returns the Spanish translation of the page ``` → [Supporting Multiple Languages](/settings/config/translate) --- ## Attribute Key-Name Style Controls the naming convention used when auto-generating handles (the programmatic key names used to access content fields in API responses and templates). When you create a field or model and provide a display name, SleekCMS auto-populates the handle based on the convention selected here. For example, a field named "Hero Image" might produce a handle of `hero_image`, `heroImage`, or `hero-image` depending on the selected style. This setting applies to auto-generated handles only. You can always override the auto-generated handle with a custom value when creating or editing a field or model. Changing this setting does not retroactively rename existing handles — it only affects newly created fields and models going forward. → [Key Naming Default Style](/settings/config/key-style) --- ## Display Name The display name of your content site. This is the human-readable label that appears in the SleekCMS interface — in the site switcher, the dashboard, and anywhere the site is identified by name. The display name is purely cosmetic and does not affect URLs, API endpoints, or content delivery. Change it at any time without impacting your content, templates, or integrations. --- ## Site Clone Token A clone token allows anyone who has it to create a complete copy of your site — including all models, content, and site builder code (templates and assets). Each clone token corresponds to a snapshot of the site at the moment the token was created. Changes made to the original site after the token was generated are not reflected in clones made from that token. Clone tokens are useful for creating starter templates, sharing site architectures with clients or team members, or bootstrapping new projects from an existing site structure. Treat clone tokens with the same care as credentials — anyone with the token can duplicate your entire site. --- ## Delete Site Permanently deletes the content site and all associated data — models, content, entries, templates, assets, media, and configuration. This action is irreversible. There is no recovery mechanism once a site is deleted. Before deleting, ensure you have exported any content or templates you want to preserve. If you are using the site builder, consider downloading the generated site as a ZIP archive before deletion. → [Static Site Downloads](/builder/deploy/download) --- ## What's Next - [Image Processing using Imgix](/settings/config/imgix) — Configuring Imgix for on-the-fly image optimization. - [Supporting Multiple Languages](/settings/config/translate) — Setting up locales and managing translated content. - [Key Naming Default Style](/settings/config/key-style) — Choosing a handle naming convention for your site. - [Site Members](/settings/members) — Managing user roles and access for your content site. - [Site Builder](/builder) — The integrated static site builder for generating and deploying websites. - [Content API](/publish/api) — REST endpoints for delivering content to your applications. Site members are the users who have access to a content site in SleekCMS. Every member is assigned one of two roles — admin or editor — which determines what they can see and do within the site. Admins have full control. Editors have content access only, and their permissions can be further restricted with fine-grained access controls. This page is a reference for the two roles, how they differ, and how customizable access works for editors. --- ## Roles SleekCMS uses a two-role model at the site level. Every member of a site is either an admin or an editor. ### Admin Admins have unrestricted access to the entire site. This includes content editing, content modeling, site configuration, site builder templates and assets, deployment, publishing, member management, and all other administrative functions. Admins can: - Create, edit, and delete all content — pages, entries, and media. - Create and modify content models — page models, entry models, block models, field configurations, and option sets. - Manage admin-only entries such as navigation, footer, and site settings. - Access and edit templates, assets, and layout in the site builder. - Deploy the site to connected hosting providers. - Trigger publish actions and manage environments. - Invite, remove, and configure permissions for other site members. - Access all site settings, including configuration, localization, and image processing. The admin role is intended for developers, site owners, and technical leads who need full control over both content and infrastructure. → [Admin Role](/settings/members/admin) ### Editor Editors have access to content only. They can create, edit, and manage content within the models that exist, but they cannot modify the models themselves, change site configuration, manage templates, or deploy the site. Editors can: - Create and edit pages, entries, and media. - Work with the content editor interface, including dynamic block composition, draft management, and content translations. - Preview content in the site builder's preview panel (if the site builder is enabled). Editors cannot: - Create or modify content models, fields, or option sets. - Access or edit site builder templates, layout, or assets. - Change site configuration settings. - Deploy the site or manage hosting connections. - Manage other site members or their permissions. - Access admin-only entries (unless explicitly granted through customizable access). The editor role is intended for content creators, marketers, and anyone who needs to work with content without affecting the site's structure or technical configuration. → [Editor Role](/settings/members/editor) --- ## Role Comparison | Capability | Admin | Editor | |---|---|---| | Create and edit content | Yes | Yes | | Manage media | Yes | Yes | | Preview content | Yes | Yes | | Create and modify models | Yes | No | | Manage option sets | Yes | No | | Access admin-only entries | Yes | No (unless granted) | | Edit site builder templates and assets | Yes | No | | Deploy site | Yes | No (unless granted) | | Trigger publish actions | Yes | No (unless granted) | | Manage site configuration | Yes | No | | Manage site members | Yes | No | --- ## Customizable Access for Editors By default, editors have access to all content across all models and languages. When you need tighter control — restricting an editor to specific parts of the site — you enable fine-grained access on that editor's membership. Fine-grained access is configured per editor. When enabled, the editor's permissions are restricted across four dimensions: models, languages, deploy targets, and publish triggers. Each dimension is independent — you can restrict one without affecting the others. ### Models Controls which content models the editor can access. When model restrictions are active, the editor only sees the specified page models, entry models, and block models in their content editing interface. Models not in their access list are hidden entirely — the editor cannot see, create, or edit content for those models. This is useful when different editors own different parts of the site. A blog editor might have access to the Blog Post page model and the Author entry model, but not to the Product page model or the Site Settings entry. A marketing editor might have access to Landing Page models and CTA block models, but not to the documentation section. ### Languages Controls which languages the editor can work with. When language restrictions are active, the editor only sees content in the specified locales. They can create and edit translations for their assigned languages but cannot access or modify content in other locales. This is useful for multi-language sites with regional content teams. A Spanish content team might have access only to the `es` locale, while a German team has access only to `de`. Each team works in their language without seeing or accidentally modifying content in other languages. Language restrictions only apply when localization is enabled for the site. → [Supporting Multiple Languages](/settings/config/translate) ### Deploy Targets Controls whether the editor can deploy the site to specific hosting targets. By default, editors cannot deploy at all. When deploy access is granted through fine-grained controls, you specify which deploy targets the editor can trigger — for example, allowing deployment to a staging environment but not to production. This is useful when content teams need to push previews to a staging URL without having access to the production deployment pipeline. → [Deploying Sites](/builder/deploy) ### Publish Triggers Controls whether the editor can execute publish triggers. Publish triggers are automated actions that fire when content is published — webhook notifications, cache invalidations, or external integrations. By default, editors cannot trigger these actions. When publish trigger access is granted, you specify which triggers the editor can fire. This is useful when certain publish workflows should only be initiated by senior editors or specific team members, while other editors can create and edit content without triggering downstream automation. → [Automating using Triggers](/publish/triggers) --- ## How Fine-Grained Access Works Fine-grained access is an opt-in restriction system, not a permission grant system. The distinction matters: **Without fine-grained access enabled**, an editor has access to all content, all languages, and no deploy or publish capabilities. This is the default and works well for small teams or sites where every editor works on everything. **With fine-grained access enabled**, the editor's access is scoped to what you explicitly configure. Any dimension you restrict narrows the editor's view. Dimensions you leave unrestricted remain fully open. For example, enabling fine-grained access and restricting only models means the editor sees a filtered set of models but can still work in all languages. Enabling fine-grained access and restricting only languages means the editor sees all models but only in their assigned locales. Fine-grained access is configured per editor — each editor on a site can have a different set of restrictions. This lets you tailor access to each team member's responsibilities without creating additional roles. → [Customizable Access](/settings/members/access-control) --- ## What's Next - [Admin Role](/settings/members/admin) — Full details on admin capabilities and responsibilities. - [Editor Role](/settings/members/editor) — Full details on editor capabilities and content access. - [Customizable Access](/settings/members/access-control) — Configuring fine-grained permissions for individual editors. - [Site Configuration](/settings/config) — Site-level settings managed by administrators. - [Organization and Sites](/org/sites) — Managing multiple sites and members at the organization level. - [Member Management](/org/members) — Inviting and managing members across your organization.