# SleekCMS All In One Documentation for AI Agents --- # SleekCMS Overview 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](/integrate/api) — REST and GraphQL endpoints for headless consumption. --- # Getting Started 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 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/fields/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/fields/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. --- # Page Models 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](/integrate/api) → [@sleekcms/client](/integrate/api#client-library) --- ## 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/stack) — Deep dive into configuring and using dynamic block fields. - [Content Editing](/content) — The editing experience for content creators working with page models. --- # Entry 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](/integrate/api) → [@sleekcms/client](/integrate/api#client-library) --- ## 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. --- # Block 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/stack) --- ## 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](/integrate/api) → [@sleekcms/client](/integrate/api#client-library) --- ## 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/stack) — Deep dive into configuring dynamic block fields and the editor experience. - [Group Fields](/model/fields/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. --- # Option Sets and Lists 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](/integrate/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](/integrate/api) — REST and GraphQL endpoints for retrieving option sets and content. --- # Content Field Types 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/stack) ### 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/fields/group) — Deep dive into organizing fields with groups and column layouts. - [Collection Fields](/model/fields/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/stack) — 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. --- # Group 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/fields/collection) — Repeatable inline structures for variable-length lists. - [Block Models](/model/blocks) — Reusable, composable content components for dynamic page sections. - [Dynamic Blocks](/model/fields/stack) — 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. --- # Collection 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](/integrate/api) → [@sleekcms/client](/integrate/api#client-library) --- ## 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/fields/group) --- ## What's Next - [Group Fields](/model/fields/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/stack) — 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 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/stack) — 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/stack) — 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/fields/group) — Non-repeatable field containers for organizing related fields. - [Collection Fields](/model/fields/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. --- # Stack A **stack** is a heterogeneous, ordered list of blocks. Each item in the list picks its own block model — one item can be a hero, the next a quote, the next a CTA — and the editor controls the composition and order. Stacks are the field type to reach for when a page section needs to mix different reusable components in an editor-chosen sequence. This page covers the model syntax, how stack content is shaped, how it renders, and how stacks fit alongside other structural field types like groups, collections, and block fields. > **Note on terminology.** Stack is the current composable-blocks field type in SleekCMS. An older concept called "dynamic blocks" has been **deprecated** — stacks replace it. If you encounter older docs or templates referencing "dynamic blocks," they describe the deprecated field; the current product uses `stack`. --- ## Model Syntax A stack is declared with the bare keyword `stack` — no parentheses, no square brackets. Stacks are always multi-item by definition, so there's no separate "single vs. multiple" toggle. ``` { title: text, sections: stack } ``` That's the entire configuration. There is **no per-field allow-list** of block types — any block defined under `models/blocks/` is available in any stack. The block library is the menu; the stack draws from all of it. If you want only one type repeated, that's a different field — use `[block(key)]`. If you want a one-off shape on a single page, use a collection `[{ ... }]`. Stacks are specifically for "this section can be one of several block types, in editor-chosen order." → [Choosing Between Structural Fields](#choosing-between-structural-fields) --- ## Content Shape A stack field's content is an array of objects. Each object is shaped like the block model it instantiates, plus a `_block` marker carrying that block's key as a plain string: ```json { "title": "About us", "sections": [ { "_block": "hero", "heading": "Care that feels personal", "subheading": "A small team focused on long-term relationships.", "background": "pexels:doctor" }, { "_block": "cta", "label": "Book a visit", "link": "/contact" }, { "_block": "quote", "body": "The most thoughtful clinic I've been to.", "attribution": "M. Castillo" } ] } ``` The `_block` value is the **block's key** — `"hero"`, `"cta"`, `"quote"` — never a numeric id. The CMS resolves the key to an internal id on save. The remaining fields on each item must match the block model's schema. A `hero` item must have the fields declared in `models/blocks/hero.model`; a `cta` item must match `models/blocks/cta.model`. The order of items in the array determines render order. --- ## Rendering A stack renders through the same `render()` helper as other block fields. Pass the whole array and `render()` dispatches per `_block`, looking up `blocks/.ejs` for each item: ```ejs

<%= item.title %>

<%- render(item.sections) %> ``` Each item flows through its own block template with the item's data exposed as `item`. The page template doesn't need to know — or branch on — what types of blocks are present; the dispatch is implicit. An optional separator can be inserted between rendered items: ```ejs <%- render(item.sections, '
') %> ``` → [Block Models](/model/blocks) → [Template Context and Data Access](/builder/code/context) --- ## Stacks in the Editor In the editor, a stack field is presented as a vertical list of block instances. Editors: - **Add** a new block by picking a block model from the full block library. - **Reorder** items by dragging them up or down. - **Hide** an item — the data is kept, but the item is excluded from rendering and from API responses. - **Remove** an item entirely. - **Edit** any item's fields inline. - **Title** an item with an editor-only label so the collapsed list is scannable ("Spring Campaign Hero", "Free Trial CTA"). These titles are UI-only and don't appear in the published output or in API responses. The editing surface is the same regardless of which block models are present — adding a new block model to your site makes it instantly available in every stack on every model. → [Content Editing](/content) --- ## Stacks via the Content API In the content API, stacks come through as the same array shape stored on the record. Each item carries the `_block` marker and the block's resolved field data: ```typescript const page = client.getPage('/about'); // page.sections → [ // { _block: 'hero', heading: 'Care that feels personal', ... }, // { _block: 'cta', label: 'Book a visit', link: '/contact' }, // { _block: 'quote', body: '...', attribution: 'M. Castillo' } // ] ``` Your frontend dispatches on `_block` to pick the right component: ```tsx const components = { hero: HeroSection, cta: CTABanner, quote: PullQuote, }; function Sections({ sections }) { return sections.map((item, i) => { const Component = components[item._block]; return Component ? : null; }); } ``` Image, video, file, reference, and other field types inside each item are resolved the same way they are on any other record — image objects are full `{ url, raw, alt, ... }` objects, references are full entry objects inline, and so on. --- ## Where Stacks Can Live Stacks can be declared on **page models** and **entry models**. They **cannot** be declared inside a **block model** — block models cannot contain stacks (or other blocks). This keeps the dispatch tree shallow and the editor predictable. A stack item, however, may internally contain: - Groups (`{ ... }`) — non-repeatable nested objects. - Collections (`[{ ... }]`) — repeatable inline structures. - Entry references (`entry(key)`, `[entry(key)]`). It may **not** contain other stacks or block fields. If you find yourself wanting nested stacks, restructure the parent block to use groups or collections instead. → [Block Models](/model/blocks) → [Group Fields](/model/fields/group) → [Collection Fields](/model/fields/collection) --- ## Choosing Between Structural Fields Stacks sit at one end of a decision tree. The right choice depends on whether the shape is one-off or reusable, whether each item is the same shape or different, and whether the content is local to one place or shared across many. | When the section is… | Reach for… | |---|---| | A one-off shape used on one model only | A group `{ ... }` (single instance) or `[{ ... }]` (repeatable) | | A reusable shape, every item the same type | `block(key)` (single instance) or `[block(key)]` (repeatable list of the same block) | | A reusable shape, **each item can be a different block** chosen by the editor | **`stack`** | | Shared content referenced from many records | `entry(key)` or `[entry(key)]` | The defining property of a stack is **heterogeneity**: items in the same field can be different block models, picked by the editor at content-edit time. If every item is the same block, prefer `[block(key)]` — it's a tighter contract for both the editor and the template. → [Block Fields](/model/fields/block) → [Collection Fields](/model/fields/collection) → [Group Fields](/model/fields/group) --- ## A Complete Example A landing page model with a fixed hero and a stack body: **`models/pages/landing.model`** ``` { title: text, hero: block(hero), body: stack } ``` **`models/blocks/hero.model`** ``` { heading: text, subheading: paragraph, background: image } ``` **`models/blocks/feature_grid.model`** ``` { heading: text, items: [ { icon: image, title: text, description: paragraph } ] } ``` **`models/blocks/cta.model`** ``` { label: text, link: link } ``` **`content/pages/landing.json`** ```json { "title": "New product launch", "hero": { "heading": "Meet Atlas", "subheading": "A new platform for distributed teams.", "background": "pexels:office team" }, "body": [ { "_block": "feature_grid", "heading": "Built for the way you work", "items": [ { "icon": "iconify:mdi:bolt", "title": "Fast", "description": "..." }, { "icon": "iconify:mdi:lock", "title": "Secure", "description": "..." } ] }, { "_block": "cta", "label": "Start free trial", "link": "/signup" } ] } ``` **`pages/landing.ejs`** ```ejs <% title(item.title) %> <%- render(item.hero) %>
<%- render(item.body) %>
``` The `hero` field is a single `block(hero)` — always present, always a hero, no editor choice. The `body` field is a stack — editors compose it from any block in the library, in any order they choose. --- ## What's Next - [Block Fields](/model/fields/block) — Single block fields and how they differ from stacks. - [Block Models](/model/blocks) — Defining the block types stacks draw from. - [Group Fields](/model/fields/group) — Non-repeatable field containers for organizing related fields. - [Collection Fields](/model/fields/collection) — Repeatable inline structures for variable-length lists of the same shape. - [Content Field Types](/model/fields) — The full set of field types available in page, entry, and block models. - [Template Context and Data Access](/builder/code/context) — The `render()` helper and other template utilities. --- # Content Editing 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/stack) → [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/fields/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) --- ## 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) → [Compare Content](/content) → [Environments & Content Versions](/integrate/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](/site/members#admin) → [Entry Models](/model/entries) --- ## What's Next - [Draft Entries](/content) — Managing draft and published states for collection records. - [Content Translations](/content) — Creating and managing content in multiple languages. - [Compare Content](/content) — Side-by-side comparison across languages and environments. - [Content Modeling](/model) — How models define the editing experience. - [Dynamic Blocks](/model/fields/stack) — Composing pages from block sections. - [Site Builder](/builder) — The integrated build and preview system. - [Content API](/integrate/api) — How edited content flows to your frontend. --- # Media Management 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](/site/config#image-processing-with-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](/integrate/api) → [@sleekcms/client](/integrate/api#client-library) --- ## 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](/site/config#image-processing-with-imgix) — Configuring image processing settings for your site. --- # Image Management & Optimization 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](/site/config#image-processing-with-imgix) — Configuring default image processing settings for your site. - [Content API](/integrate/api) — Retrieving images and content through the API. - [@sleekcms/client](/integrate/api#client-library) — The `getImage()` method and image handling in the JavaScript client. --- # Site Builder 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/architecture) --- ## 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](/builder/code/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](/integrate/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) → [Local Workspace (cms-sync)](/builder/code/ide) → [Build Configuration and Environment](/builder/config) --- ## What's Next - [Integrated JAMstack](/builder/architecture) — The static generation philosophy and how SleekCMS implements it. - [Model Templates](/builder/code/main) — Binding EJS templates to page, entry, and block models. - [Layout](/builder/code/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. - [Local Workspace (cms-sync)](/builder/code/ide) — The CLI for editing your site locally with VS Code, Cursor, or any AI agent. - [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. --- # Site Builder Architecture 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/architecture) --- ## 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](/builder/code/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/stack) — Configuring composable block sections. - [@sleekcms/client](/integrate/api#client-library) — The JavaScript/TypeScript client with matching helper methods. --- # Online Coding Environment The site builder includes a full coding environment in the browser. You write EJS templates, CSS, JavaScript, and config files directly in the SleekCMS interface — no local setup, no Git checkout, no install step. Saves trigger an immediate rebuild and the result is visible in preview seconds later. This page covers what the editor exposes, how files are organized, and how to work with the environment day-to-day. --- ## What's in the Editor The coding environment surfaces every file that participates in rendering, organized into the same logical structure described in the [Site Builder reference](/builder): - **`pages/`** — EJS templates for page models. One template per page model, named to match the model key. - **`entries/`** — EJS templates for entry models that need their own rendered fragment. - **`blocks/`** — EJS templates for block models. Each declared block has a template here. - **`layouts/`** — Layout wrappers. The default is `common.ejs`; you can add more and select per page. - **`css/`** — Stylesheets. `tailwind.css` is special — see [Tailwind and Styling](/builder/config/tailwind). - **`js/`** — Client-side scripts. - **`public/`** — Other static assets (favicons, fonts, downloadables) — see [Assets and Static Files](/builder/assets). The file tree mirrors the file naming convention used throughout SleekCMS. A key like `blog+` produces files at `models/pages/blog+.model`, `pages/blog+.ejs`, and content files under `content/pages/blog+/`. The editor groups these by purpose so you can move between schema, template, and sample content without losing track. --- ## The Save-Rebuild-Preview Loop Every save triggers a build. The cycle is: 1. **Save** — Edit a template or asset, save the file. 2. **Sync** — The CMS syncs the file change and validates it against the model and template engine. 3. **Build** — Affected pages re-render. For most edits this is incremental — only pages that touch the changed file rebuild. 4. **Preview** — The preview view updates with the rebuilt output. The loop is designed for tight iteration. Single-page edits typically complete in a second or two. Layout-wide changes may rebuild more pages but still finish before you're done switching tabs. When something fails — a template throws, an image shortcut doesn't resolve, a model field doesn't match its content — the error is written to `sync-errors.log` in the workspace root. Check that file first when a save doesn't appear in preview. → [Site Builder](/builder) --- ## Editor Surfaces The coding environment is split into a few surfaces that map to what you're working on: **File tree** — Navigation across templates, assets, layouts, and configuration. Files are grouped by type and sorted by key. **Code editor** — A standard monospace editor with syntax highlighting for EJS, HTML, CSS, JavaScript, and JSON. Tab handling, search, and multi-cursor work as you'd expect. **Preview pane** — The rendered output of the current page (or a selected page if you're editing a block, layout, or asset). The pane updates after each save. **Console / error surface** — Build errors, template runtime errors, and image-resolution warnings surface here in addition to `sync-errors.log`. The split is configurable — you can collapse the file tree, hide the preview when working on shared assets, or expand the preview full-width when reviewing layout work. --- ## What You Can Edit The coding environment treats templates and assets as the contract between models and rendered output. You can: - Add and edit page/entry/block templates ([Model Templates](/builder/code/main)). - Edit the layout wrapper ([Layout](/builder/code/layout)). - Add CSS and JS files in `css/` and `js/`, include them with `link()` and `script()`. - Add static files to `public/` ([Assets and Static Files](/builder/assets)). - Adjust `tailwind.css` for design-token changes ([Tailwind and Styling](/builder/config/tailwind)). You can't directly edit the model schemas from the coding environment — those live in the modeling interface. The coding environment is for *rendering*, not for shaping content structure. → [Content Modeling](/model) --- ## Editing Locally with cms-sync For longer editing sessions, multi-file refactors, or AI-agent-driven site building, SleekCMS provides **cms-sync** — a CLI that pulls your site to a local folder, watches for changes, and syncs every save back to the CMS. You edit in VS Code, Cursor, or any editor; the CLI handles the round trip. Run it directly without installing: ```bash npx @sleekcms/sync@latest -t <your-auth-token> ``` Get the token from the **Set Up Local Workspace** option in the SleekCMS builder. cms-sync also injects `AGENT.md`, `CLAUDE.md`, and a Copilot instructions file into the workspace so AI agents (Cursor, Claude Code, Copilot) understand your site from the first prompt. → [Local Workspace (cms-sync)](/builder/code/ide) --- ## What's Next - [Model Templates](/builder/code/main) — Binding EJS templates to page, entry, and block models. - [Layout](/builder/code/layout) — The wrapper template that frames every page. - [Template Context and Data Access](/builder/code/context) — The full reference for `item`, helpers, and content access. - [Assets and Static Files](/builder/assets) — Managing CSS, JS, and other static files. - [Tailwind and Styling](/builder/config/tailwind) — Zero-setup Tailwind support. - [Local Workspace (cms-sync)](/builder/code/ide) — The CLI for editing your site locally with VS Code, Cursor, or any AI agent. --- # Model Templates Model templates are EJS files that turn content into HTML. Every page model, entry model, and block model can be bound to a template, and the binding is implicit — the template's filename matches the model's key. When the builder renders a record, it runs the matching template with the record's content as `item`. This page covers the three template types, the naming convention that links templates to models, what each template receives, and a few rules to keep in mind when writing them. --- ## Three Template Types The site builder distinguishes three template categories, each living under a different folder: | Folder | Used for | When it renders | |---|---|---| | `pages/` | Page model templates | Once per page record — produces an HTML file at the record's path | | `blocks/` | Block model templates | Each time a block instance is rendered via `render()` | | `entries/` | Entry model templates | Each time an entry is rendered via `render()` (optional — entries are usually consumed as data) | A page template is the entry point for an HTML file. A block template is invoked by a parent template that calls `render()` on a block value. An entry template is the same idea but for entries — useful when an entry needs a consistent rendered fragment across many parent contexts (a team-member card, an author bio). → [Site Builder Architecture](/builder/architecture) --- ## Naming Convention The link between a model and its template is the **key** — the filename without the extension. A page model at `models/pages/about.model` has its template at `pages/about.ejs`. A block model at `models/blocks/hero.model` has its template at `blocks/hero.ejs`. For collections, the `+` suffix is part of the key and appears on every file: | Model file | Template file | |---|---| | `models/pages/about.model` | `pages/about.ejs` | | `models/pages/blog+.model` | `pages/blog+.ejs` | | `models/entries/testimonials+.model` | `entries/testimonials+.ejs` | | `models/blocks/hero.model` | `blocks/hero.ejs` | The CMS resolves the pairing automatically. There's no template registration step — adding a `.ejs` file with the right name and a matching `.model` is the entire binding. For pages whose key contains `_`, the underscore maps to `/` in the URL (e.g., `docs_getting-started.ejs` → `/docs/getting-started`). The key still matches the model file one-for-one. --- ## What Each Template Receives Every template is called with the current record as `item`, plus the full site content as named variables and helper functions: **`item`** — The current record's content. In a page template, `item` is the page record (with `_path`, `_slug`, and `_meta`). In a block template, `item` is the block instance. In an entry template, `item` is the entry. **Site-wide context** — `pages`, `entries`, `images`, and `options` are available everywhere. Most templates use the helper functions (`getPage`, `getPages`, `getEntry`, `getImage`, `getOptions`) instead of touching these directly. **Helpers** — `render()` for delegating to other templates, `src()`/`img()`/`picture()`/`svg()` for images, `embed()` for videos, `path()` and `url()` for links, and head-injection helpers (`title`, `meta`, `link`, `script`, `style`). The full reference is on [Template Context and Data Access](/builder/code/context). --- ## A Page Template A page template renders a single page's HTML. The simplest possible page template: ```ejs <% title(item.title) %> <h1><%= item.title %></h1> <%- marked(item.content) %> ``` Most page templates do a bit more — they include SEO metadata, render block stacks, pull in shared entries, and link assets: ```ejs <% title(item.title + ' | My Site') %> <% meta({ name: 'description', content: item.summary }) %> <% link({ rel: 'canonical', href: url(item) }) %> <article> <h1><%= item.title %></h1> <time datetime="<%= item.published_at %>"><%= item.published_at %></time> <% if (item.author) { %> <p>By <%= item.author.name %></p> <% } %> <%- img(item.hero, '1200x600') %> <%- render(item.body) %> </article> ``` The page template doesn't define the outer HTML shell — `<html>`, `<head>`, navigation, footer — that's the layout's job ([Layout](/builder/code/layout)). --- ## A Block Template A block template renders one block instance. It receives the block's field data as `item` and outputs the HTML for that block: ```ejs <section class="hero" style="background-image: url('<%- src(item.background, '1920x800') %>')"> <h1><%= item.heading %></h1> <p><%= item.subheading %></p> <% if (item.cta_link) { %> <a href="<%= item.cta_link %>"><%= item.cta_label %></a> <% } %> </section> ``` When a page contains a block field or a stack, the page template calls `render()` to dispatch to the block template: ```ejs <%- render(item.hero) %> <!-- single block(hero) field --> <%- render(item.sections) %> <!-- stack field — dispatches per _block --> ``` Each rendered block gets its own `item` scope, so a block template can be developed in isolation from the pages that use it. → [Block Fields](/model/fields/block) → [Stack](/model/fields/stack) --- ## An Entry Template Entry templates are optional. Most entries are consumed as data (`item.author.name`, `getEntry('footer').copyright_text`) — you don't need a template for an entry just to read its fields. You add an entry template when the entry has a recurring visual representation that you want to centralize: ```ejs <!-- entries/people+.ejs --> <article class="person"> <%- picture(item.photo, '128x128') %> <h3><%= item.name %></h3> <p class="role"><%= item.role %></p> <p><%= item.bio %></p> </article> ``` Then any template can render an entry through it: ```ejs <% for (const person of getEntry('people+')) { %> <%- render(person) %> <% } %> ``` If an entry doesn't have a template, calling `render()` on it returns an empty string — no error. --- ## Rules to Keep in Mind A few patterns trip up most first templates: **Escape user content, output helper HTML unescaped.** Use `<%= %>` for text fields (`title`, `name`) and `<%- %>` for helpers that return HTML (`render`, `img`, `picture`, `marked`). **`markdown` fields are raw markdown, not HTML.** Wrap with `marked()`: `<%- marked(item.content) %>`. `richtext` fields are already HTML — use `<%-` directly. **Page templates don't define `<html>` or `<head>`.** That's the layout's job. A page template renders the page body; the layout wraps it. See [Layout](/builder/code/layout). **Don't inline `<link>` or `<script>` tags.** Use the head-injection helpers `link()` and `script()` — they deduplicate and respect ordering. **Templates can call any helper from any depth.** A block template can call `getEntry()`, set page-level metadata via `title()`/`meta()`, and pull in shared options. The full template context is available everywhere. → [Template Context and Data Access](/builder/code/context) --- ## What's Next - [Layout](/builder/code/layout) — The wrapper that frames every page. - [Template Context and Data Access](/builder/code/context) — The complete reference for `item`, helpers, and content access. - [Block Models](/model/blocks) — Defining blocks that templates render. - [Stack](/model/fields/stack) — Composable block sections and dispatch via `render()`. - [Site Builder Architecture](/builder/architecture) — The build pipeline from content to HTML. --- # Layout (Wrapper) A layout is the outer HTML shell every page is wrapped in — the `<!DOCTYPE html>`, `<head>`, navigation, and footer that appear on every page of the site. Page templates fill in the body; the layout supplies everything around it. By centralizing the shell in one file, you change site chrome once and the change propagates everywhere. This page covers where layouts live, how they receive page output, the `main` variable, the default layout, and how to use multiple layouts when sections of your site need different chrome. --- ## Where Layouts Live Layouts live under `layouts/` as `.ejs` files. The default layout is `common.ejs` — every page uses it unless you specify otherwise: ``` layouts/ common.ejs Default layout — applied to all pages by default marketing.ejs Optional alternate layout bare.ejs Optional minimal layout (e.g., for landing pages) ``` You add layouts simply by creating new `.ejs` files in the folder. There's no registration step — the filename is the layout key. --- ## The `main` Variable A layout receives one thing that page templates don't: the **`main`** variable. `main` is the rendered output of the page template — already-converted HTML, ready to be dropped into the layout's body: ```ejs <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> <body> <header> <!-- ...nav... --> </header> <main> <%- main %> </main> <footer> <!-- ...footer... --> </footer> </body> </html> ``` `main` is only meaningful inside a layout template — not inside page, block, or entry templates. Use `<%-` (unescaped) to output it; it's already HTML. The same `item` you'd access in the page template is also available in the layout — both run with the current page record as `item`. This means the layout can look at the page's metadata, conditionally adjust chrome, or pull in entries based on page fields. --- ## A Practical Layout A typical layout pulls navigation and footer content from entries, sets baseline head metadata, and includes site-wide assets: ```ejs <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <% link({ rel: 'icon', href: src(getImage('favicon'), '32x32') }) %> </head> <body> <% const header = getEntry('header'); %> <header> <a href="/" class="logo"> <%- img(getImage('logo'), '180x40') %> </a> <nav> <% for (const link of header.nav_links) { %> <a href="<%= link.href %>"><%= link.label %></a> <% } %> </nav> </header> <main> <%- main %> </main> <% const footer = getEntry('footer'); %> <footer> <p><%- footer.copyright %></p> </footer> </body> </html> ``` Notice that `<title>` is **not** set here — page templates set the title themselves via `title()`, and the layout doesn't override it. The same goes for `description`, OG tags, and canonical URLs. The layout sets defaults (viewport, charset, favicon); pages set per-page metadata. → [Template Context and Data Access](/builder/code/context) --- ## Head Injection Across Layout and Page The head-injection helpers — `title()`, `meta()`, `link()`, `style()`, `script()` — collect everything called from any template (layout, page, block, entry) and place it correctly in the final HTML. This means a block template deep inside the page can declare its own stylesheet: ```ejs <!-- blocks/code-sample.ejs --> <% link('/css/syntax.css') %> <pre class="code-sample"><code><%= item.source %></code></pre> ``` …and the layout doesn't have to know about it. The CSS link appears in the final `<head>` once per build, deduplicated, regardless of how many code-sample blocks the page contains. Use the layout for **site-wide** assets and metadata. Use page templates for **per-page** metadata. Use block templates for **per-block** assets. The injection system reconciles all three. --- ## Choosing a Different Layout Most pages use `common.ejs` by default. To use a different layout, set it via a comment at the top of the page template: ```ejs <% // layout: marketing.ejs %> <h1><%= item.title %></h1> <%- render(item.sections) %> ``` Or to opt out of a layout entirely (for special outputs like `rss.xml`, `llm.txt`, or `sitemap.xml`): ```ejs <%# layout: none %> <?xml version="1.0" encoding="UTF-8"?> ... ``` When a page opts out, the template's output is the file's content — no wrapper. This is the right choice for XML, plain-text, and any file where injecting a `<!DOCTYPE html>` would break the contract. --- ## When to Use Multiple Layouts A single `common.ejs` works for most sites. Add additional layouts only when a section of the site truly needs different chrome: - **Marketing landing pages** with no nav/footer for distraction-free conversion. - **App-style admin or dashboard** views with a different shell than the public site. - **Documentation** with a sidebar/TOC layout while the rest of the site uses a centered marketing layout. For one-off differences (a hero section vs. no hero), don't make a new layout — conditionally render inside `common.ejs` based on a page field. Layouts are for *shape*, not *variants*. --- ## What's Next - [Model Templates](/builder/code/main) — Page, block, and entry templates that fill in `main`. - [Template Context and Data Access](/builder/code/context) — The full reference for `main`, `item`, helpers, and head injection. - [Assets and Static Files](/builder/assets) — CSS, JS, and other files referenced from the layout. - [Site Builder Architecture](/builder/architecture) — How layout, page, and block templates compose during a build. --- # Template Context and Data Access 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/stack) → [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](/integrate/api#client-library) --- ## 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](/builder/code/layout) — The shared layout template that wraps page output. - [Site Builder](/builder) — The overall build pipeline and preview system. - [Dynamic Blocks](/model/fields/stack) — How `render()` powers composable page layouts. - [Content Field Types](/model/fields) — The field types that determine `item` data shapes. - [Content API](/integrate/api) — How the same content structure flows through the API. - [@sleekcms/client](/integrate/api#client-library) — The JavaScript client with matching method signatures. --- # Local Workspace (cms-sync) The browser-based coding environment is enough for many edits. For substantial template work, multi-file refactors, or building entire sites with AI agents, drop into a local workspace using **cms-sync**, the SleekCMS CLI. It downloads your site's models, templates, content, and assets to a local folder, watches for changes, and pushes saves back to SleekCMS in real time. You edit in VS Code, Cursor, or any editor; the CLI handles the round trip. This page covers what cms-sync is, the one-line install-and-run, what it sets up locally, the AI-agent instruction files it injects, and the sync mechanics. --- ## What cms-sync Is `cms-sync` is a Node CLI distributed as [`@sleekcms/sync`](https://www.npmjs.com/package/@sleekcms/sync) on npm. It does three things: 1. **Pulls** your full site state — models, templates, content (including markdown bodies), CSS, JS, and the workspace's AI-agent reference files — into a local folder. 2. **Watches** the folder. Every save streams the changed file to the SleekCMS API; the server validates and rebuilds. 3. **Cleans up** on exit. Workspaces are ephemeral — your edits live on the server, and the next run re-fetches a fresh copy. The package is `@sleekcms/sync`; the binary it installs is `sleekcms`. The same CLI is what the **Set Up Local Workspace** option in the SleekCMS builder hands you on a copy-paste command. --- ## Quickstart The fastest way to get started — no install, run directly with `npx`: ```bash npx @sleekcms/sync@latest -t <your-auth-token> ``` Grab the auth token from the **Builder** area of your site in the SleekCMS dashboard (the **Set Up Local Workspace** option shows the exact command with your token pre-filled). That's the whole setup. The CLI: 1. Authenticates and fetches your site to a local folder named after the site. 2. Writes `AGENT.md`, `CLAUDE.md`, and `.vscode/copilot-instructions.md` so any AI agent picks up the SleekCMS reference. 3. Offers to open the folder in VS Code or Cursor if either is detected. 4. Starts watching. Every save pushes to SleekCMS within seconds. While watching, two keys are bound: | Key | Action | |---|---| | `r` | Re-fetch all files from the server (re-sync) | | `x` or `Ctrl+C` | Exit and clean up the local workspace | --- ## Install Globally (Optional) If you'll use the CLI often, install once globally and drop the `npx` prefix: ```bash npm install -g @sleekcms/sync sleekcms -t <your-auth-token> ``` The global binary is `sleekcms`. Both `npx @sleekcms/sync` and `sleekcms` accept the same options. ### Command Options | Flag | Description | |---|---| | `-t, --token <token>` | API authentication token (required) | | `-p, --path <dir>` | Custom workspace directory (default: a folder named after your site/token) | | `-v, --version` | Print the CLI version | A non-default environment is encoded in the token itself, so you usually only need `--token`. --- ## What Gets Created Locally The first run creates a workspace folder with the full site structure plus the AI-agent reference files: ``` <site-name>/ ├── AGENT.md Generic agent reference ├── CLAUDE.md Claude / Claude Code reference ├── .vscode/ │ ├── copilot-instructions.md GitHub Copilot in VS Code │ └── settings.json Editor settings │ └── src/ ├── models/ │ ├── pages/<key>.model Page schemas │ ├── entries/<key>.model Entry schemas │ └── blocks/<key>.model Block schemas │ ├── views/ │ ├── pages/<key>.ejs Page templates │ ├── entries/<key>.ejs Entry templates │ ├── blocks/<key>.ejs Block templates │ └── layouts/<name>.ejs Layout wrappers │ ├── content/ │ ├── pages/<key>.json Single page content │ ├── pages/<key+>/<slug>.json Collection page items │ ├── pages/<key+>/<slug>.md Markdown collection items (when the model qualifies) │ ├── entries/<key>.json Single entry content │ ├── entries/<key+>.json Collection entry content │ └── images.json Site-level reusable images │ └── public/ ├── css/<name>.css Stylesheets (tailwind.css is special) └── js/<name>.js Scripts ``` This is the same file layout used everywhere in the SleekCMS reference. Edits to any of these files sync back to the CMS. --- ## AI-Agent Workflow The headline feature of cms-sync — beyond plain local editing — is that **it sets up your workspace for AI agents to build and maintain your site**. On every run, the CLI injects three reference files: | File | Picked up by | |---|---| | `AGENT.md` | Copilot agent mode, any generic agent that reads `AGENT.md` | | `CLAUDE.md` | Claude / Claude Code | | `.vscode/copilot-instructions.md` | GitHub Copilot in VS Code | Each file contains the full SleekCMS reference: file naming conventions, model syntax, template helpers, content shapes, the image-shortcut convention, the rules for what's allowed where. An AI session opened in this workspace knows your site — it doesn't have to be re-taught how the platform works. ### Generating a Site with AI Open the workspace in Cursor, VS Code with Copilot, or Claude Code, and describe the site you want: ``` Build a portfolio site with: - A home page with a hero section, featured projects, and a contact form - A blog with individual post pages - A shared header and footer - Tailwind CSS styling - SEO meta tags on every page ``` The AI creates models, templates, layouts, content, and assets in the right places. As each file lands on disk, the watcher pushes it to SleekCMS. The site is live before the agent finishes summarizing what it built. ### Editing an Existing Site The same instruction files apply when an agent is making changes to an existing site. Ask it to add a dark-mode toggle, refactor a block, generate ten more blog posts, or build out a new page type — it has the full SleekCMS reference plus the existing site's files for context. The site is **plain files** — EJS, JSON, CSS, JS. No proprietary format, no lock-in. Anything the AI writes is the same kind of file you'd write by hand. --- ## How Sync Works After the initial fetch, cms-sync runs a local file watcher (debounced to batch rapid edits). When you save a file: 1. The watcher detects the change. 2. The file is classified — model, template, content record, CSS, or JS. 3. It's pushed to the SleekCMS API in dependency order (models first, then templates, then content). 4. The SleekCMS server validates, rebuilds, and the preview reflects the change. ``` Your editor → save → watcher → SleekCMS API → rebuild → preview ``` The CLI keeps a local `.cache/` folder tracking the server-known state of each file. Only real diffs are pushed, so no redundant API calls on incidental saves (formatter pass, whitespace cleanup, etc.). ### Pull vs. Push The sync is two-way: - **Push** happens automatically on every save in the local workspace. - **Pull** happens on startup (initial fetch) and on demand via the `r` key. Use `r` after editing content in the SleekCMS dashboard to get those changes into the local files. --- ## Ephemeral Workspaces By default, the workspace folder is **wiped when the CLI exits** (`Ctrl+C` or `x`). This is intentional — your edits already live on the server, and a fresh re-run downloads them again. There's no risk of the local copy drifting from the server. This means: - Don't store edits-in-progress in the workspace without saving them. A save pushes; an unsaved edit is lost on exit. - If you want a persistent local copy (for a Git-tracked snapshot, for example), use `-p <dir>` to write to a directory you control and skip the auto-cleanup. The CLI still pushes on save; the folder just persists between runs. --- ## Pairing with VS Code, Cursor, Other Editors When the CLI starts, it offers to open the workspace in VS Code or Cursor if either is detected on your PATH. You can also open the folder manually in any editor — vim, Zed, Sublime, JetBrains, anything that opens a folder. cms-sync watches the filesystem, not your editor, so editor choice is purely personal. The `.vscode/` folder is created automatically with sensible defaults and the Copilot instructions file. Other editors don't need any configuration — they just open the folder. --- ## Common Workflows **Single small edit** — Open the SleekCMS browser editor directly; don't bother spinning up the CLI for one template tweak. **Multi-file refactor** — Run cms-sync, open in VS Code/Cursor, do the multi-file edit, exit. The CLI ensures every save pushes; the workspace cleans up afterward. **Building a new site from a prompt** — Run cms-sync on an empty (or near-empty) site. Open in Cursor or Claude Code. Describe the site. Let the agent generate models, templates, and content. **Maintaining an AI-built site** — Same workspace, any AI session. The instruction files in the workspace mean any new agent session — today, next week, next year — picks up the same site context. **Local Git history** — Use `-p <dir>` pointed at a Git-tracked folder. Each run pulls the latest server state into the folder; you commit between runs to keep a versioned history. Note that the server is the source of truth — `git push` doesn't push to SleekCMS, the CLI does. --- ## Troubleshooting | Problem | Fix | |---|---| | Authentication error | Verify your token in the SleekCMS dashboard (Builder → Set Up Local Workspace). | | No files downloaded | Token may be missing the environment suffix or scoped to a different site. | | Changes not syncing | Check the terminal output. Saves are debounced by a few seconds. | | Workspace disappeared after exit | Expected — workspaces are ephemeral by default. Re-run the CLI to pull a fresh copy. Use `-p <dir>` to keep the folder. | | Wrong site opened | Each token maps to one site. Switch sites by switching tokens. | For build-side issues (template errors, model mismatches, image resolution failures), check `sync-errors.log` in the workspace root — this is the same error log surfaced in the CMS, mirrored locally. --- ## Prerequisites - **Node.js v16 or later** — install via [nodejs.org](https://nodejs.org), Homebrew (`brew install node`), or your distribution's package manager. - **A SleekCMS auth token** — generated in the site's Builder area, available on the **Set Up Local Workspace** dialog. That's it. No global config files, no environment variables to set, no project bootstrapping. --- ## What's Next - [Online Coding Environment](/builder/code) — The browser-based editor for quick edits. - [Model Templates](/builder/code/main) — The template files you'll be editing. - [Layout](/builder/code/layout) — The wrapper template. - [Template Context and Data Access](/builder/code/context) — The complete API surface for templates. - [Site Builder Architecture](/builder/architecture) — How the templates you edit locally compose during a build. --- # Assets and Static Files Templates render the HTML. Assets — CSS, JavaScript, fonts, favicons, downloads — provide the styling, interactivity, and supporting files that templates pull in. SleekCMS organizes assets into a few well-known folders and gives templates two helpers, `link()` and `script()`, for including them on a page. This page covers where assets live, how to include them, the special handling for Tailwind CSS, and how the build pipeline handles static files at deploy time. --- ## Folder Layout Assets are organized by type: ``` css/ Stylesheets — included via link() tailwind.css Special: auto-compiled and auto-injected (see below) styles.css Your own CSS file(s) js/ Client-side scripts — included via script() app.js carousel.js public/ Other static files served verbatim at the site root favicon.ico apple-touch-icon.png fonts/inter-var.woff2 downloads/brochure.pdf robots.txt ``` The `css/` and `js/` folders are conventional locations for stylesheets and scripts that templates include. The `public/` folder is for any other static file that needs to ship with the site — favicons, manifest files, fonts, downloadables, `robots.txt`, `sitemap.xml`, anything that's served as-is at a known URL. At deploy time, every file under these folders ends up in the deployed output. The directory structure is preserved. --- ## Including CSS Use the `link()` helper to add a stylesheet. Pass the path; the helper figures out the right `<link>` tag attributes: ```ejs <% link('/css/styles.css') %> <!-- Output: <link rel="stylesheet" type="text/css" href="/css/styles.css"> --> ``` For non-CSS link types (canonical, preconnect, RSS feed autodiscovery), pass an object with explicit attributes: ```ejs <% link({ rel: 'canonical', href: url(item) }) %> <% link({ rel: 'preconnect', href: 'https://fonts.googleapis.com' }) %> <% link({ rel: 'alternate', type: 'application/rss+xml', title: 'RSS', href: '/rss.xml' }) %> ``` Don't write raw `<link>` tags. The injection helpers deduplicate (so calling the same stylesheet from many block templates is safe) and place tags in the right `<head>` order. --- ## Including JavaScript Use the `script()` helper. A path ending in `.js` becomes an external script; anything else is treated as inline code: ```ejs <!-- External --> <% script('/js/app.js') %> <!-- Output: <script src="/js/app.js"></script> --> <!-- Inline --> <% script('console.log("loaded")') %> <!-- Output: <script>console.log("loaded")</script> --> <!-- With explicit attributes --> <% script({ src: '/js/app.js', defer: true }) %> <% script({ src: '/js/module.js', type: 'module' }) %> ``` Scripts are appended at the end of `<body>` rather than in `<head>`, which is the right place for most page scripts (no render-blocking). --- ## Tailwind Is Special `/css/tailwind.css` is the one stylesheet that does **not** need to be included via `link()`. If that file exists, the build pipeline compiles Tailwind utility classes used in your templates and auto-injects the resulting stylesheet on every page. You don't call `link('/css/tailwind.css')` — doing so would add the file twice. For everything you'd customize in a Tailwind project — theme tokens, custom utilities, plugin imports — edit `/css/tailwind.css` directly. The compile pipeline picks up the changes on save. → [Tailwind and Styling](/builder/config/tailwind) --- ## Files in `public/` Anything you drop under `public/` is served at the corresponding URL. A file at `public/favicon.ico` is available at `/favicon.ico`. A file at `public/fonts/inter.woff2` is at `/fonts/inter.woff2`. A file at `public/downloads/menu.pdf` is at `/downloads/menu.pdf`. You don't need a helper to reference these — write the URL directly in your templates: ```ejs <% link({ rel: 'icon', href: '/favicon.ico' }) %> <style> @font-face { font-family: 'Inter'; src: url('/fonts/inter-var.woff2') format('woff2-variations'); } </style> <a href="/downloads/menu.pdf">Download menu (PDF)</a> ``` Use `public/` for: - Favicons (`favicon.ico`, `apple-touch-icon.png`) - PWA manifests (`manifest.json`) - Self-hosted fonts (`.woff2`) - Static downloadables (PDFs, sample files) - Hand-written `robots.txt` or `sitemap.xml` (though the latter is usually better as a page template) - Anything else that ships as-is --- ## Asset URLs and Caching The deployed asset URLs are stable — `/css/styles.css` stays `/css/styles.css` across builds. The build pipeline does not append content hashes by default, so CDN caching is up to the deployment target. Most deployment targets (Netlify, Vercel, surge.sh) handle cache headers automatically based on file type. For more aggressive cache control, configure the deploy target's cache settings rather than touching the file output. --- ## Common Patterns **Layout includes site-wide assets.** The layout is the right place for stylesheets and scripts that every page needs: ```ejs <% link('/css/styles.css') %> <% script('/js/app.js') %> ``` **Pages opt into page-specific assets.** A page that needs a special library or stylesheet declares it in the page template: ```ejs <% link('/css/blog.css') %> <% script('/js/comments.js') %> ``` **Blocks declare their own dependencies.** A block that needs a syntax highlighter or a charting library declares it where the block is defined: ```ejs <!-- blocks/code-sample.ejs --> <% link('/css/syntax.css') %> <% script('/js/highlight.js', 'defer') %> <pre class="code"><code><%= item.source %></code></pre> ``` Even if the page contains five code-sample blocks, the link and script are emitted once per page — the injection helpers deduplicate automatically. --- ## What's Next - [Tailwind and Styling](/builder/config/tailwind) — Zero-setup Tailwind support and customization. - [Layout](/builder/code/layout) — Where site-wide assets are usually declared. - [Online Coding Environment](/builder/code) — Adding and editing assets in the browser editor. - [Template Context and Data Access](/builder/code/context) — Full reference for `link()`, `script()`, and other head-injection helpers. --- # Build Configuration and Environment Build configuration controls how the site builder compiles and serves your site. Most of these settings sit at the site level and apply to every build — image processing, locale support, key-naming defaults, and similar baseline choices. This page covers what's configurable, where each setting lives, and which configuration belongs to the builder versus the site as a whole. --- ## Two Kinds of Configuration SleekCMS splits build-related configuration across two surfaces: **Site Configuration** ([Site Configuration](/site/config)) — The site-level toggles: whether the site builder is enabled, the image processing backend (default vs. Imgix), localization, key-naming style, and similar choices. These settings are global across the site. **Builder-specific config** — In-template settings authored directly in EJS or assets: layout selection, head injection, CSS/JS includes, Tailwind customization. These are per-template or per-block decisions. The split is intentional. Site-level toggles are admin decisions that affect the whole site; template-level choices are developer decisions made where the code lives. → [Site Configuration](/site/config) --- ## Enabling the Site Builder The site builder is a per-site toggle. When enabled, templates compile into static HTML and the deploy targets become available. When disabled, the site operates as a pure headless CMS — content flows out through the API, and there's no built-in HTML rendering. You can enable or disable the builder at any time without losing content. Disabling hides the template editor and deploy options but leaves all your content, models, and assets in place. → [Site Configuration](/site/config) --- ## Image Processing The default image backend handles transformations on the fly — resizing, format conversion, cropping, color adjustments — through query parameters on the image URL. This is documented fully on [Image Management & Optimization](/media/images). Sites can opt to route image requests through **Imgix** instead. Imgix is a third-party image processing service; configuring it requires an Imgix account and a web-folder source pointing at your image origin. With Imgix enabled, the same transformation query parameters work, but the URLs route through Imgix's CDN. Switch image backends at the site-configuration level — templates don't change. The same `src()`, `img()`, `picture()`, and `svg()` helpers produce URLs for whichever backend is active. → [Site Configuration → Image Processing with Imgix](/site/config#image-processing-with-imgix) --- ## Localization Localization is a site-level toggle. When enabled, content can be edited and delivered in multiple locales — each content record stores translated values, and the API and builder pick the right translation based on the requested language. Templates don't need to be duplicated per language. The same template renders content in any locale; the differences appear in the data, not in the templates. → [Site Configuration → Localization and Translations](/site/config#localization-and-translations) --- ## Key-Naming Style When you create a field or model in the editor, SleekCMS auto-generates the handle (the programmatic key used in API responses and templates) from the display name. The naming style controls the convention: `snake_case`, `camelCase`, or `kebab-case`. This is purely a default. Auto-generated handles can always be edited manually, and changing the site-level default doesn't rename existing handles. → [Site Configuration → Attribute Key-Name Style](/site/config#attribute-key-name-style) --- ## Layouts and Per-Page Configuration The builder's per-page configuration lives in the page templates themselves, not in a separate config file. The main knobs: **Layout selection** — Comment in a page template chooses which layout wraps the page: ```ejs <% // layout: marketing.ejs %> ``` Or opt out of layouts entirely (for XML, plain-text outputs): ```ejs <%# layout: none %> ``` **Head metadata** — Page-specific titles, descriptions, and OG tags via `title()`, `meta()`, `link()` from the template. **Asset includes** — `link()` and `script()` calls in the template control which CSS and JS files the page pulls in. → [Layout](/builder/code/layout) → [Assets and Static Files](/builder/assets) --- ## Tailwind Configuration Tailwind is enabled by the presence of `/css/tailwind.css`. The file itself acts as the configuration surface — theme tokens, plugin imports, and custom utilities are all declared in this file using Tailwind v4's CSS-first config approach. → [Tailwind and Styling](/builder/config/tailwind) --- ## What's Not Configurable A few things are intentionally not surfaced as config: - **The build engine.** EJS is the only template language. There's no swap-to-Pug or swap-to-Liquid setting. - **Routing.** Page URLs are derived from page-model file keys (`_` → `/`, `+` for collections). There's no routes file. - **Asset bundling rules.** No webpack config, no Vite config — the build pipeline handles compilation internally. This is by design. Reducing the configuration surface keeps sites portable and makes the platform easier to reason about. If you find yourself wanting a knob that doesn't exist, the answer is usually a template- or asset-level workaround rather than a config setting. --- ## What's Next - [Site Configuration](/site/config) — The site-level toggles for builder, image processing, localization, and key style. - [Tailwind and Styling](/builder/config/tailwind) — The Tailwind setup. - [Layout](/builder/code/layout) — Per-page layout selection. - [Assets and Static Files](/builder/assets) — Including CSS, JS, and static files. - [Site Builder](/builder) — The overall build pipeline. --- # Zero-setup Tailwind and Styling The site builder ships with zero-setup Tailwind CSS. You don't install Node, don't configure PostCSS, and don't run a build command. Create a file at `/css/tailwind.css`, write Tailwind utility classes in your templates, and the resulting compiled stylesheet is auto-injected on every page. This page covers how Tailwind is enabled, how the auto-injection works, where to customize the theme and add plugins, and how to opt out if you'd rather use your own stylesheet. --- ## Enabling Tailwind Tailwind is enabled by the **presence** of `/css/tailwind.css`. Create the file with any content — the simplest valid setup is one line: ```css @import "tailwindcss"; ``` That's it. Saving the file kicks off the compile pipeline, and from the next build forward, the compiled Tailwind stylesheet is automatically injected into every page's `<head>`. **Do not call `link('/css/tailwind.css')` from your templates.** The auto-injection handles inclusion. Calling `link()` would emit the link tag twice. If `/css/tailwind.css` doesn't exist, Tailwind is not enabled — utility classes in your templates will have no effect. --- ## How the Auto-Injection Works When the builder runs, it scans every template (`pages/`, `blocks/`, `entries/`, `layouts/`) for Tailwind utility classes. It collects every class found, compiles the corresponding CSS, and produces the final `tailwind.css` output. That compiled stylesheet is then injected into every page's `<head>` via a `<link>` tag managed by the builder. Only utility classes that are actually used end up in the output. The compiled stylesheet is small — typically a few KB — because Tailwind's tree-shaking only includes classes the build saw. You don't have to declare which files Tailwind should scan. The builder knows where templates live and scans them all automatically. --- ## Customizing the Theme The `/css/tailwind.css` file is also the place to customize the theme — colors, spacing, fonts, breakpoints. Tailwind v4 uses CSS-first configuration via the `@theme` directive: ```css @import "tailwindcss"; @theme { --color-brand: #4f46e5; --color-brand-light: #818cf8; --color-brand-dark: #3730a3; --font-family-display: 'Inter', system-ui, sans-serif; --spacing-72: 18rem; --spacing-84: 21rem; } ``` These tokens become utility classes — `bg-brand`, `text-brand-light`, `font-display`, `p-72`. Adding a token is the entire workflow; there's no separate `tailwind.config.js` to maintain. For semantic color tokens that switch with light/dark themes, declare CSS variables and reference them from `@theme`: ```css :root { --surface: #ffffff; --text: #111827; } :root[data-theme="dark"] { --surface: #0f172a; --text: #f8fafc; } @theme { --color-surface: var(--surface); --color-text: var(--text); } ``` Now `bg-surface` and `text-text` will pick up the appropriate variable for the active theme. --- ## Adding Plugins Tailwind plugins are imported with `@plugin` directives in the same file: ```css @import "tailwindcss"; @plugin "@tailwindcss/forms"; @plugin "@tailwindcss/typography"; ``` The builder resolves and loads the plugin during compilation. Common Tailwind plugins (`@tailwindcss/forms`, `@tailwindcss/typography`) work out of the box; community plugins work if they're published as standard npm packages. --- ## Custom Components and Utilities For reusable component classes or one-off utilities, use the standard Tailwind directives: ```css @import "tailwindcss"; @layer components { .btn-primary { @apply inline-flex items-center px-4 py-2 rounded-lg bg-brand text-white font-medium; @apply hover:bg-brand-dark transition-colors; } } @layer utilities { .glass { backdrop-filter: blur(12px); background: rgba(255, 255, 255, 0.1); } } ``` Anything you'd put in a normal Tailwind CSS entry file works here. The file is processed the same way Tailwind v4 processes any project's entry stylesheet. --- ## Using Tailwind Alongside Other CSS Tailwind is additive. You can have hand-written CSS in `/css/styles.css` (or any other `.css` file under `css/`) alongside Tailwind. Use `link()` to include those stylesheets: ```ejs <% link('/css/typography.css') %> <% link('/css/forms.css') %> ``` Tailwind is still auto-injected separately. The order of the final `<head>` puts auto-injected Tailwind first, followed by linked stylesheets in the order they were declared. This means your own stylesheets can override Tailwind defaults without `!important`. --- ## Opting Out To use the site builder without Tailwind, simply don't create `/css/tailwind.css`. Utility classes won't compile, and you can use entirely hand-written CSS via `link()`-included stylesheets. If you start with Tailwind and later want to remove it, deleting `/css/tailwind.css` disables the auto-injection — utility classes in your templates will be ignored. Replace them with regular CSS classes before deleting the file to avoid losing styling. --- ## A Complete Example A minimal `tailwind.css` for a docs site: ```css @import "tailwindcss"; @plugin "@tailwindcss/forms"; :root { --surface: #ffffff; --surface-elevated: #f9fafb; --text: #111827; --text-muted: #6b7280; --border: #e5e7eb; --accent: #6366f1; } :root[data-theme="dark"] { --surface: #0f172a; --surface-elevated: #1e293b; --text: #f8fafc; --text-muted: #94a3b8; --border: #334155; --accent: #818cf8; } @theme { --color-surface: var(--surface); --color-surface-elevated: var(--surface-elevated); --color-text: var(--text); --color-text-muted: var(--text-muted); --color-border: var(--border); --color-accent: var(--accent); --font-family-display: 'Inter', system-ui, sans-serif; } @layer components { .doc-link { @apply text-accent hover:underline underline-offset-2; } } ``` Templates can then use `bg-surface`, `text-text-muted`, `font-display`, `doc-link`, and any other Tailwind utility — all without configuring a build pipeline. --- ## What's Next - [Assets and Static Files](/builder/assets) — Including additional CSS and JS files alongside Tailwind. - [Layout](/builder/code/layout) — Where site-wide assets and Tailwind classes are typically declared. - [Build Configuration](/builder/config) — Other site-level build settings. - [Site Configuration](/site/config) — Site-level toggles. --- # Form Backend for Websites SleekCMS includes a form backend built into the deployed site. Any `<form>` with a `data-sleekcms="<name>"` attribute is captured automatically — submissions stream into the CMS without a separate backend service, an action URL, or any JavaScript glue. No serverless functions, no webhook wiring, no third-party form service. This page covers how form capture works in the deployed site, the markup contract, what's stored on each submission, and how the form-builder side of the CMS connects to the submissions inbox. --- ## The Markup Contract A form is captured if it has a `data-sleekcms` attribute set to a form name: ```html <form data-sleekcms="contact"> <label> Name <input name="name" type="text" required> </label> <label> Email <input name="email" type="email" required> </label> <label> Message <textarea name="message" rows="5"></textarea> </label> <button type="submit">Send</button> </form> ``` Three things to know: 1. **The form name** (`data-sleekcms="contact"`) groups submissions. Each unique name becomes a separate inbox in the CMS — `contact`, `newsletter`, `quote-request`, `event-rsvp`, whatever names you choose. 2. **Field names** (`name="email"`) become the keys on each submission. Use standard, descriptive names. 3. **No `action` attribute is required.** The site builder handles submission routing automatically. You can leave `action` off entirely (the form submits to the same URL by default) and the SleekCMS sync layer intercepts the request. That's the whole contract. Add the attribute, add named inputs, and submissions are captured. --- ## How Submissions Are Captured When a visitor submits the form on the deployed site, the SleekCMS forms backend: 1. Receives the form POST. 2. Validates the form name and basic field presence. 3. Stores the submission against the form, indexed by submission time. 4. Returns a success response (or an error if validation fails). Submissions land in the CMS's Forms section, where you can review them, search across them, and export them. → [Form Submissions](/forms) --- ## Default vs. Custom Submission Pages By default, after successful submission the user is shown a built-in success page provided by the SleekCMS forms backend. To customize the post-submit experience, add a hidden input named `redirect` with the URL to send users to: ```html <form data-sleekcms="contact"> <input type="hidden" name="redirect" value="/thank-you"> <!-- other fields --> </form> ``` After a successful submission, the visitor is redirected to `/thank-you`. Use this to send users to a thank-you page you've built as a normal SleekCMS page. --- ## Field Types and Storage Any standard HTML form input is captured: `<input>` (all types), `<textarea>`, `<select>`, `<input type="checkbox">`, `<input type="radio">`, `<input type="file">`. The value is stored as it arrives on the server: - Text-like fields → string values - Checkboxes → present-or-absent - Multi-select / multi-checkbox with the same name → array of values - File uploads → stored against the submission, downloadable from the inbox There's no schema declared for the form's fields on the SleekCMS side — the form is the schema. Adding a new input adds a new field to subsequent submissions. Removing an input from the HTML doesn't drop the field from past submissions; it just stops new ones from having that key. --- ## Spam and Validation The backend applies sensible defaults for spam protection: - **Honeypot field** — Adding a hidden field named `_gotcha` (or any name starting with `_`) catches naive bots that fill in every field. Submissions with the honeypot filled are silently dropped. - **Rate limiting** — Submissions are rate-limited per form per IP. - **Required field validation** — HTML5 `required` attributes are enforced on the client side; the backend additionally validates that fields marked required have non-empty values. For stronger spam protection, integrate Cloudflare Turnstile, reCAPTCHA, or hCaptcha as a hidden field on the form. The backend doesn't validate the challenge response itself — that's typically done via a publish trigger or a downstream system reading the submission inbox. --- ## Working with Submissions Submissions appear in the SleekCMS sidebar under the Forms section, grouped by form name. From there you can: - View the full submission with all fields. - Search across submissions for a specific email, phone number, or text fragment. - Export submissions to CSV. - Delete submissions individually or in bulk. Submissions are **read-only** — they're a record of what the visitor sent, captured at submit time. They cannot be edited from the inbox. → [Form Submissions](/forms) --- ## Access Control By default, only admins see the Forms section. To grant access to editors, configure fine-grained access on the editor's membership and explicitly enable the Forms scope. → [Site Members](/site/members) --- ## Two Sides of "Forms" in SleekCMS There are two places "Forms" shows up in the CMS, and they serve different purposes: | Where | What it's for | |---|---| | `/builder/forms` (this page) | The **form authoring** side — designing the form markup in your site code. | | `/forms` | The **submission inbox** — viewing and managing what visitors have sent. | The two are loosely coupled by the `data-sleekcms` form name. Renaming a form in your code effectively starts a new submission group; previous submissions stay attached to the old name. → [Form Submissions](/forms) --- ## A Practical Example A newsletter signup that redirects to a thank-you page, has a honeypot, and uses Tailwind for styling: ```html <form data-sleekcms="newsletter" class="flex flex-col gap-3 max-w-md"> <input type="hidden" name="redirect" value="/newsletter/thanks"> <input type="text" name="_gotcha" class="hidden" tabindex="-1" autocomplete="off"> <label class="flex flex-col gap-1"> <span class="text-sm font-medium">Email</span> <input type="email" name="email" required class="rounded-md border border-border px-3 py-2" placeholder="you@example.com" > </label> <button type="submit" class="rounded-md bg-accent text-white px-4 py-2 font-medium" > Subscribe </button> </form> ``` Submissions to this form land in the Forms section under `newsletter`. Each submission has an `email` field. Visitors who submit are redirected to `/newsletter/thanks`. Bots that fill the `_gotcha` field are dropped silently. --- ## What's Next - [Form Submissions](/forms) — The inbox side of the same feature. - [Site Members](/site/members) — Granting editors access to submissions. - [Automating using Triggers](/integrate/triggers) — Firing a trigger when new submissions arrive (e.g., to forward to email or a webhook). - [Assets and Static Files](/builder/assets) — Including CSS for form styling. --- # Deploying Sites Once the site builder has compiled your content into HTML, deployment is the last step — getting that HTML onto a public URL. SleekCMS gives you a few ways to do this without leaving the CMS: deploy directly to Netlify or surge.sh, download the built site as a ZIP and host anywhere, or push to your own pipeline by triggering an external build via a [publish trigger](/integrate/triggers). This page covers the overall deploy model, the supported targets, and how to choose between them. --- ## The Deploy Model Every deploy starts the same way: the builder takes the current state of your content and templates, compiles them into a complete static site (HTML files, CSS, JS, images, assets), and then pushes that output to a destination. The build is fully contained — there's no external runtime, no server-side rendering, no database lookups on the deployed site. The deployed output is just files. Because the output is pure static files, you can host it almost anywhere. SleekCMS supports two first-party push targets (Netlify and surge.sh) and a downloadable ZIP that works with every other static host on the planet. > **Beyond the static site:** If your frontend is built outside SleekCMS — a Next.js app, an Astro site, a mobile app — the deploy story is different. The build happens in your repo, and SleekCMS notifies it when content changes via a [publish trigger](/integrate/triggers). The output isn't a SleekCMS-built static site; it's whatever your own build produces. --- ## Supported Push Targets ### Netlify A first-class integration. You authenticate SleekCMS with your Netlify account, choose a site, and every deploy from the CMS pushes the built output to that Netlify site. The result is live on your Netlify URL within seconds. Netlify also gives you custom domains, HTTPS, edge caching, and redirects out of the box — managed in the Netlify dashboard. → [Deploy site via Netlify](/builder/deploy/netlify) ### surge.sh A lightweight target for quick deploys and shareable URLs. Surge serves your site under a `*.surge.sh` subdomain (or your own custom domain). The setup is minimal — no account configuration, no project linking. Surge is a good fit for prototypes, internal demos, and any site where you want a free, fast public URL without setting up a host. → [Deploy site via surge.sh](/builder/deploy/surge) ### Download as ZIP Click the download option and the CMS builds a fresh copy of the site, packages every file as a ZIP, and hands you the archive. From there you can upload it to any static host — Cloudflare Pages, Vercel, AWS S3 + CloudFront, GitHub Pages, your own server, anywhere. This is the universal escape hatch. If a host accepts a folder of static files, the ZIP works. → [Static Site Downloads](/builder/deploy/download) --- ## Triggers for External Builds When your frontend lives outside SleekCMS — a Next.js app on Vercel, an Astro site on Cloudflare Pages, anything that builds in your own pipeline — deploys aren't initiated from the SleekCMS deploy section. Instead, you wire up a [publish trigger](/integrate/triggers) that calls your external build system whenever content changes. The pattern: 1. Editor publishes content in SleekCMS. 2. SleekCMS fires the configured trigger (a GitHub Actions workflow dispatch, a Netlify build hook, a custom webhook). 3. The external system pulls fresh content from the SleekCMS API and rebuilds the site. This is what you'd reach for if your codebase is the source of truth and SleekCMS is just the headless content layer. The deploy section is for sites built **by** SleekCMS; triggers are for sites built **with** SleekCMS content. → [Automating using Triggers](/integrate/triggers) → [Content API](/integrate/api) --- ## Choosing a Deploy Target | Goal | Use | |---|---| | Production site hosted on Netlify | [Netlify](/builder/deploy/netlify) | | Quick public preview / prototype | [surge.sh](/builder/deploy/surge) | | Host on Cloudflare Pages, Vercel, S3, GitHub Pages, custom server | [Download as ZIP](/builder/deploy/download) | | Frontend lives in your own repo (Next.js, Astro, etc.) | [Publish triggers](/integrate/triggers) | Most sites use one of the first three for their production environment and download the ZIP occasionally for archiving or for offline review. --- ## What's in the Deployed Output Every deploy contains: - One HTML file per page record (static pages and collection items). - Compiled CSS — Tailwind output (if `tailwind.css` exists) plus any other stylesheets in `css/`. - All JavaScript files in `js/`. - All files in `public/` (favicons, fonts, downloadables) at their original paths. - All images referenced from content, served from the image CDN (not copied into the output). Images are not bundled into the deployed output — they stay on the SleekCMS image CDN. This keeps deployments small and gives you the on-the-fly transformation capability documented in [Image Management](/media/images). The deployed HTML references images via their CDN URLs. The same is true for hosted videos and uploaded files — they live on their respective CDNs and are referenced by URL. --- ## Cache and Invalidation Deploys produce fresh files every time. Cache invalidation is the host's responsibility: - **Netlify** invalidates cached pages on every deploy automatically. - **surge.sh** serves the latest deploy with minimal caching. - **Custom hosts** (from a ZIP) follow whatever cache policy you've configured on that host. For sites consumed via the API rather than the deploy step, see [Environments & Content Versions](/integrate/envs) for the equivalent invalidation mechanics on the content side. --- ## What's Next - [Deploy site via Netlify](/builder/deploy/netlify) — First-class integration with Netlify. - [Deploy site via surge.sh](/builder/deploy/surge) — Fast, public URLs for quick deploys. - [Static Site Downloads](/builder/deploy/download) — ZIP export for hosting anywhere. - [Automating using Triggers](/integrate/triggers) — For frontends built outside SleekCMS. - [Content API](/integrate/api) — The other delivery path: structured JSON to your own frontend. --- # Static Site Downloads The download option packages the entire built site as a ZIP archive. It's the universal deploy escape hatch: if a host accepts a folder of static files, the download works. Cloudflare Pages, Vercel, AWS S3, GitHub Pages, Render, a server in your basement — they all serve a SleekCMS-built site without any platform-specific integration. This page covers what's in the ZIP, how to deploy it, and a few common hosts with specific guidance. --- ## What's in the ZIP A download contains a complete static site: ``` my-site/ ├── index.html Homepage ├── about/index.html Static page ├── blog/ │ ├── index.html Collection index page (if defined) │ ├── hello-world/index.html │ └── nextjs-tips/index.html ├── css/ │ ├── tailwind.css Compiled Tailwind (if enabled) │ └── styles.css Your own CSS files ├── js/ │ └── app.js ├── favicon.ico ├── fonts/ │ └── inter-var.woff2 └── rss.xml Any custom page outputs (RSS, sitemap, etc.) ``` Every URL on your deployed site corresponds to a file in the ZIP. Page paths follow the directory-with-`index.html` convention (`/about` → `about/index.html`) which is the universally supported way to serve clean URLs from static hosts. **Images are not in the ZIP.** Image content is served from the SleekCMS image CDN — your deployed HTML references those CDN URLs directly. This keeps downloads small and preserves the on-the-fly transformation capability. The same is true for hosted videos and uploaded files: they stay on their respective CDNs. --- ## Generating a Download From the deploy section in the CMS: 1. Click **Download** (or the equivalent download option). 2. The CMS triggers a fresh build using the current content and templates. 3. When the build completes, the ZIP is offered as a browser download. You can download as often as you want. Each download is a fresh build with the current state of your content. --- ## Deploying the ZIP The deployment steps depend on your host. A few common patterns: ### Cloudflare Pages 1. From the Cloudflare dashboard, create a new Pages project. 2. Choose the "Direct Upload" option (not the Git-connected option). 3. Upload the contents of the ZIP (or upload the ZIP itself — Cloudflare extracts it). 4. The site is served at `<project>.pages.dev` (or your custom domain). Subsequent deploys: drag-and-drop a new ZIP into the same project, or use the `wrangler` CLI for scripted uploads. ### Vercel 1. Install Vercel CLI: `npm i -g vercel`. 2. Extract the ZIP locally and `cd` into the folder. 3. Run `vercel`. Follow the prompts; choose a project name; deploy. Subsequent deploys: `vercel --prod` from the same folder. ### AWS S3 + CloudFront 1. Create an S3 bucket configured for static website hosting. 2. Extract the ZIP and upload the contents to the bucket (e.g., `aws s3 sync ./extracted-site s3://your-bucket --delete`). 3. Front the bucket with CloudFront for HTTPS and edge caching. 4. Invalidate the CloudFront distribution on each deploy (`aws cloudfront create-invalidation --distribution-id XXX --paths "/*"`). ### GitHub Pages 1. Extract the ZIP. 2. Push the extracted folder's contents to the root of a `gh-pages` branch (or to the configured Pages source branch) in a GitHub repository. 3. Enable Pages on the repo. The site is served at `<user>.github.io/<repo>`. ### Your own server Extract the ZIP and serve the folder from any static-file-capable web server — nginx, Caddy, Apache, even Python's `http.server` for testing. Configure the server to serve `index.html` for directory requests and handle 404s by serving your `404.html` (if you've defined one). A minimal nginx config: ```nginx server { listen 80; server_name example.com; root /var/www/my-site; index index.html; error_page 404 /404.html; location / { try_files $uri $uri/index.html =404; } } ``` --- ## Setting the Site Origin If your deployed URL differs from any defaults (e.g., you're deploying to a custom domain like `https://docs.example.com`), set the **site origin** in [Site Configuration](/site/config) before downloading. The `url()` template helper uses the origin when constructing absolute URLs in your HTML — canonical links, OG image URLs, RSS feed URLs, and `<base>` tags. Setting it correctly before download ensures the deployed HTML references the right public URLs. --- ## Automating ZIP-Based Deploys For hosts that don't have first-class SleekCMS integration but where you want automated deploys, you have two paths: 1. **Manual download per release.** Download, upload to the host. Works fine for low-frequency deploys. 2. **API-driven build trigger from your CI.** Use the [content API](/integrate/api) to fetch site content into your own build pipeline, run a Node-based EJS builder there, and deploy from CI. This is essentially "build outside SleekCMS using SleekCMS content" — see [Automating using Triggers](/integrate/triggers) for how to wire publish events to a CI build. For most use cases, the first path is sufficient. The download option is fast (typically seconds), and most static hosts accept drag-and-drop uploads. --- ## What's Next - [Deploying Sites](/builder/deploy) — Overview of all deploy targets. - [Deploy site via Netlify](/builder/deploy/netlify) — First-class Netlify integration. - [Deploy site via surge.sh](/builder/deploy/surge) — Quick public URLs. - [Automating using Triggers](/integrate/triggers) — Triggering external builds from publish events. - [Site Configuration](/site/config) — Setting the site origin for absolute URLs. --- # Deploy site via Netlify Netlify is a first-class deploy target in SleekCMS. You connect your Netlify account once, select a site, and every deploy from the CMS pushes the freshly built output to that Netlify site. Domains, HTTPS, edge caching, and redirects come from Netlify's platform; SleekCMS handles the build and the push. This page covers the setup, what happens on each deploy, working with custom domains, and a few notes on cache and redirects. --- ## Setup The connection has three steps: 1. **Authorize SleekCMS with Netlify.** From the deploy section, follow the "Connect Netlify" flow. You'll be redirected to Netlify, sign in, and authorize SleekCMS to deploy on your behalf. The OAuth token is stored encrypted on your site. 2. **Pick a Netlify site.** Choose an existing Netlify site to deploy to, or create a new one inline. The new-site flow asks for a site name (which becomes the default `*.netlify.app` subdomain) and creates the empty site in your Netlify account. 3. **Deploy.** From the SleekCMS deploy section, click Deploy. The CMS builds the static output and pushes it to the linked Netlify site. After the initial setup, deploys are one click — no further configuration needed. --- ## What Happens on Each Deploy Each deploy from SleekCMS to Netlify follows this sequence: 1. The site builder compiles the current state of your content and templates into static HTML, CSS, JS, and assets. 2. SleekCMS packages the output and uploads it to Netlify via Netlify's deploy API. 3. Netlify processes the upload — invalidating its edge cache and rolling out the new files. 4. The deployed URL serves the new version within seconds. You'll see the deploy status update in real time in the CMS. Netlify itself also logs the deploy in your Netlify dashboard, where you can review history, roll back to a previous deploy if needed, and see the deploy diff. --- ## Custom Domains Netlify handles custom domains. From your Netlify site dashboard: 1. Add your custom domain (e.g., `example.com`). 2. Update your DNS to point to Netlify per Netlify's instructions (typically a CNAME or A record). 3. Netlify provisions an HTTPS certificate automatically via Let's Encrypt. SleekCMS doesn't need to know about the custom domain — Netlify routes the traffic and your deploys go to the same Netlify site regardless of which domain serves them. If you want absolute URLs from your templates (canonical links, OG image URLs, RSS feeds) to use the custom domain, set the **site origin** in [Site Configuration](/site/config) to the custom domain. The `url()` template helper uses that origin when constructing full URLs. --- ## Redirects and Headers Netlify supports `_redirects` and `_headers` files at the root of the deployed site for URL rewriting and HTTP header customization. To use them in SleekCMS, add them to `public/`: ``` public/ _redirects _headers ``` These files are served as-is at the deployed site's root, and Netlify reads them on each deploy. Standard Netlify syntax works — see Netlify's documentation for the full rule format. Common uses: ``` # public/_redirects /old-blog/* /blog/:splat 301 /api/* https://api.example.com/:splat 200 ``` ``` # public/_headers /* X-Frame-Options: DENY Referrer-Policy: no-referrer-when-downgrade /fonts/* Cache-Control: public, max-age=31536000, immutable ``` --- ## Triggering Deploys from Content Publish If you'd rather deploy in response to a publish action (rather than clicking Deploy in the CMS), you have two paths: 1. **From the publish flow** — When you publish content to an environment, you can select a publish trigger to fire alongside the publish. Configure a Netlify build-hook trigger so that publishing kicks off a Netlify build automatically. 2. **From the SleekCMS deploy** — The CMS deploy button stays available. Many teams use both: editors publish content (no automatic deploy), and a developer or scheduled task triggers the final SleekCMS deploy. For the trigger-based approach see [Automating using Triggers](/integrate/triggers). --- ## Rollbacks Netlify keeps the history of every deploy. From the Netlify dashboard: 1. Open the site's "Deploys" tab. 2. Find the previous deploy you want to restore. 3. Click "Publish deploy". Netlify makes that deploy current again. The next SleekCMS deploy overwrites it. SleekCMS doesn't currently expose Netlify's rollback UI in its own interface — use the Netlify dashboard directly for rollbacks. --- ## What's Next - [Deploying Sites](/builder/deploy) — Overview of all deploy targets. - [Deploy site via surge.sh](/builder/deploy/surge) — Alternative target for quick deploys. - [Static Site Downloads](/builder/deploy/download) — Manual ZIP export. - [Automating using Triggers](/integrate/triggers) — Triggering external builds and deploys on publish. - [Site Configuration](/site/config) — Setting the site origin for absolute URLs. --- # Deploy site via surge.sh [surge.sh](https://surge.sh) is a static site host that's optimized for quick, public deploys. It's the lightweight counterpart to Netlify in the SleekCMS deploy options — minimal setup, instant URLs, and a good fit for prototypes, demos, and internal previews where you just need a public link. This page covers the setup, the deploy flow, custom domains, and the trade-offs versus Netlify. --- ## Setup 1. **Authenticate.** From the deploy section, follow the surge.sh setup flow. You'll provide a surge account email or create one inline. 2. **Pick a subdomain.** Surge sites live at `<name>.surge.sh`. Choose a name that's unique (e.g., `my-site.surge.sh`, `acme-prototype.surge.sh`). If you have a custom domain, you can configure it after the initial deploy. 3. **Deploy.** Click Deploy. The CMS builds the static site and pushes it to surge. After setup, deploys are one click. --- ## What Happens on Each Deploy The build-and-push flow is similar to other targets: 1. The site builder compiles the current state of your content and templates into static HTML and assets. 2. SleekCMS pushes the output to surge. 3. Surge serves the new version at your subdomain within seconds. Surge is intentionally minimalist — there's no separate dashboard, no deploy history, no build logs to review. The current deploy is what's live; previous deploys are overwritten. --- ## Custom Domains Surge supports custom domains on free and paid plans. To use one: 1. Add a CNAME record at your DNS provider pointing your custom domain to `your-name.surge.sh`. 2. Add a file named `CNAME` in `public/` (in your SleekCMS site) containing just your custom domain on one line: ``` public/CNAME ``` ``` www.example.com ``` 3. Deploy. Surge picks up the CNAME file and serves the site at that domain. HTTPS for custom domains is available on surge's paid plans. The default `*.surge.sh` subdomain is served over HTTPS automatically. If you set a custom domain, also update the **site origin** in [Site Configuration](/site/config) so absolute URLs (canonical links, OG image URLs, RSS feed URLs) match the public URL. --- ## When to Use surge.sh vs. Netlify | Use surge.sh when | Use Netlify when | |---|---| | You want a fast public URL with no configuration | You need a production host with a dashboard and history | | Prototype, demo, internal preview, time-boxed campaign | Production marketing site, blog, customer-facing pages | | You're fine without a deploy history or rollback UI | You want one-click rollbacks and deploy diffs | | You want a memorable subdomain (e.g., `summer-launch.surge.sh`) | You want a custom domain with managed HTTPS | Many teams use both — surge for review and prototyping, Netlify for production. You can configure both targets on a single site and deploy to whichever fits. --- ## Limits and Caveats - **No deploy history.** Surge replaces the previous deploy. To roll back, you'd need to redeploy a previous content state, which isn't easy from SleekCMS — for production sites where rollback matters, use Netlify. - **No build logs.** A failed deploy returns an error but doesn't give you a per-step log. If a deploy fails, check `sync-errors.log` for build-side issues; if the build succeeded but the push failed, the error message from surge typically points at the cause. - **HTTPS on custom domains is paid.** The default `*.surge.sh` subdomain has free HTTPS; custom domains require a paid plan for HTTPS. - **No edge functions, redirects, or headers.** Surge serves static files only. If you need redirects or custom headers, use Netlify or download the ZIP and host on Cloudflare Pages or Vercel. --- ## What's Next - [Deploying Sites](/builder/deploy) — Overview of all deploy targets and how to choose. - [Deploy site via Netlify](/builder/deploy/netlify) — First-class integration with Netlify. - [Static Site Downloads](/builder/deploy/download) — ZIP export for hosting anywhere. - [Site Configuration](/site/config) — Setting the site origin to match your deployed domain. --- # Integrating Content # 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](/integrate/api) → [@sleekcms/client](/integrate/api#client-library) → [@sleekcms/react](/integrate/api#client-library) --- ## 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](/integrate/triggers) → [Customizable Access](/site/members#customizable-access-for-editors) --- ## 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](/integrate/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](/integrate/api) — Endpoint details, authentication, and response structure. - [@sleekcms/client](/integrate/api#client-library) — The official JavaScript/TypeScript client library. - [@sleekcms/react](/integrate/api#client-library) — React hooks for client-side content fetching. - [Automating using Triggers](/integrate/triggers) — Configuring webhooks, build hooks, and automated workflows. - [Environments & Content Versions](/integrate/envs) — Creating environments, promoting content, and managing snapshots. - [Site Builder](/builder) — The alternative to API integration — generating static sites directly within SleekCMS. --- # Content API 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](/integrate/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](/site/config#localization-and-translations) --- ## 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](/integrate/api#client-library) --- ## What's Next - [@sleekcms/client](/integrate/api#client-library) — Full client library reference including CDN usage and advanced configuration. - [@sleekcms/react](/integrate/api#client-library) — React hooks for client-side content fetching. - [Environments & Content Versions](/integrate/envs) — Managing content snapshots and multi-stage publishing. - [Automating using Triggers](/integrate/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. --- # Automating using Triggers 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](/site/members) → [Customizable Access](/site/members#customizable-access-for-editors) --- ## 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](/integrate/envs) --- ## What's Next - [Publishing Content](/integrate) — Overview of the content delivery pipeline. - [Content API](/integrate/api) — The API that your build systems call to fetch content after a trigger fires. - [Environments & Content Versions](/integrate/envs) — Managing content snapshots and the promotion workflow that fires triggers. - [Site Members](/site/members) — Configuring who can fire triggers. - [Deploying Sites](/builder/deploy) — Alternative deployment through the integrated site builder, without triggers. --- # Environments & Content Versions 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](/integrate/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](/integrate/api#client-library) --- ## 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](/integrate/api) — Endpoint details, response structure, and client library usage. - [Automating using Triggers](/integrate/triggers) — Triggering builds and workflows when publishing to environments. - [Publishing Content](/integrate) — Overview of the full content delivery pipeline. - [Site Configuration](/site/config) — Site-level settings and configuration. --- # Site Settings A site in SleekCMS is the container for one content surface — one set of models, one set of content records, one set of templates, and one deployment target. Site settings cover everything that controls how the site behaves at the administrative level: builder configuration, image processing, localization, team membership, clone tokens, and site lifecycle. This page orients you around the two settings screens — Site Configuration and Site Members — and what belongs where. --- ## Site Configuration The configuration screen controls the technical behavior of the site. This is where you toggle the site builder, set the image processing backend, configure localization, choose key-naming conventions, and manage the site's identity and lifecycle (display name, clone tokens, deletion). These settings affect all content and rendering on the site. They are intended for admins — editors do not see this screen. → [Site Configuration](/site/config) --- ## Site Members The members screen controls who has access to the site and what they can do. SleekCMS uses a two-role model — **admin** and **editor** — with optional fine-grained access controls on individual editors for restricting models, languages, deploy targets, and publish triggers. This is where you invite teammates, change roles, remove members, and configure per-editor access scopes. → [Site Members](/site/members) --- ## Site vs. Organization Settings divide cleanly between two levels: - **Site settings** (this section) affect a single content site — its models, content, templates, deployment, and team. - **Organization settings** ([Organization Management](/org)) affect the org as a whole — billing, the list of sites in the org, and org-wide membership. Members are managed at both levels. An organization member has access to the org dashboard and can be granted access to specific sites. A site member has access scoped to one site only. → [Organization Management](/org) --- ## What's Next - [Site Configuration](/site/config) — Builder toggles, image processing, localization, key style, and clone tokens. - [Site Members](/site/members) — Roles and fine-grained access control. - [Organization Management](/org) — Org-level dashboard, billing, and site management. - [Site Builder](/builder) — Building and deploying the site itself. --- # Site 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](/site/config#image-processing-with-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](/site/config#localization-and-translations) --- ## 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](/site/config#attribute-key-name-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](/site/config#image-processing-with-imgix) — Configuring Imgix for on-the-fly image optimization. - [Supporting Multiple Languages](/site/config#localization-and-translations) — Setting up locales and managing translated content. - [Key Naming Default Style](/site/config#attribute-key-name-style) — Choosing a handle naming convention for your site. - [Site Members](/site/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](/integrate/api) — REST endpoints for delivering content to your applications. --- # Site Members 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](/site/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](/site/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](/site/config#localization-and-translations) ### 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](/integrate/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](/site/members#customizable-access-for-editors) --- ## What's Next - [Admin Role](/site/members#admin) — Full details on admin capabilities and responsibilities. - [Editor Role](/site/members#editor) — Full details on editor capabilities and content access. - [Customizable Access](/site/members#customizable-access-for-editors) — Configuring fine-grained permissions for individual editors. - [Site Configuration](/site/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. --- # Organization Management An **organization** is the top-level container in SleekCMS. Every site belongs to one organization. Members, billing, and the list of sites all sit at the org level. This is your workspace boundary. The Organization section of the CMS is where you manage the org as a whole: the sites that belong to it, the people who can access those sites, the subscription that pays for them, and the templates available for cloning new sites. --- ## What Lives at the Org Level Four things are managed at the organization level rather than at the site level: **Sites** — The list of content sites your org owns. From here you create new sites (blank or cloned from a template), open existing sites, and configure site-level access for org members. **Members** — The people who can access the org. Org members can be granted access to any number of sites within the org. Roles at the org level are independent of roles at the site level — an org admin can see the org dashboard and billing, while a site admin only manages a single site. **Subscription** — Billing, plan, and seat management. The subscription determines how many sites the org can hold, how many members can be invited, and what features are enabled. Owners manage the subscription; admins and editors do not see this screen. **Templates** — Cloneable starter sites available to the org. The template gallery is shared across the org and includes both the public template library and any custom templates your team has saved as clonable sites. --- ## Org vs. Site The split between org-level and site-level settings is intentional: - **Org** owns: billing, the list of sites, org-wide membership, available templates. - **Site** owns: content models, content records, view templates, deployment, site-specific membership, and site configuration. A site's settings page does not show billing. The organization's dashboard does not show content models. This keeps each surface focused — content teams work inside a single site without seeing billing they don't manage, and org admins manage subscriptions and seats without scrolling past content. → [Site Settings](/site) --- ## Multiple Sites in One Org An organization can hold any number of sites — limited only by your subscription plan. Common patterns: - **Agency** — One org, many sites, one site per client. - **Multi-brand company** — One org, one site per brand, each with its own models and content. - **Staging and production** — Use [environments](/integrate/envs) within a single site rather than separate sites. Sites are independent content containers, not deployment environments. → [Sites](/org/sites) — Detailed management of the sites in your org. --- ## What's Next - [Sites](/org/sites) — Creating, opening, and managing the sites in your organization. - [Members](/org/members) — Inviting teammates and configuring org-level access. - [Subscription](/org/subscription) — Billing, plans, and seat management. - [Templates](/org/templates) — Cloning sites from templates and sharing your own. - [Site Settings](/site) — Settings scoped to a single site (config and members). --- # Organization and Sites An organization can hold any number of sites — agency clients, multi-brand portfolios, separate product surfaces, staging laboratories. Each site is an independent content container with its own models, content, templates, members, and deployment. The org gives them shared billing, shared org-level membership, and a single dashboard to switch between them. This page covers what a site is, how to create and switch between sites, and how site-level access fits with org-level access. --- ## What a Site Is A site is the unit of content in SleekCMS. Inside a site, you have: - **Models** — page, entry, and block schemas specific to that site. - **Content** — page records, entry records, and inline block data. - **Templates and assets** — the site builder's EJS templates, CSS, JS, and static files. - **Members** — site-scoped admins and editors (in addition to org-level members). - **Deploy targets** — Netlify, surge.sh, or download configurations specific to this site. - **Environments** — content version aliases for staged publishing. Two sites under the same org are fully independent. They don't share models, content, or templates. The org is just a logical and billing boundary. --- ## Creating a Site From the org's Sites screen, the **Create Site** button starts a new site. Two paths: **From a template** — Choose a template from the gallery (or paste a clone token for a private template). The new site is created as an independent copy of the template — all of its models, templates, content, and assets are duplicated. Subsequent changes to the original template don't propagate. **Blank** — Create an empty site. No models, no content, no templates. You start from scratch with the content modeler. Either path produces a fully independent site that's yours to edit. → [Templates](/org/templates) → [Getting Started](/getting-started) --- ## Switching Between Sites The site switcher in the SleekCMS interface lists every site in your current org that you have access to. Switching loads that site's models, content, builder, and members — the URL changes to include the site's slug, and the page reloads with the new site's data. If you're a member of multiple orgs, the org switcher is one level up. Most users only see one org; agencies and contractors with access to multiple orgs see them all. --- ## Site Membership vs. Org Membership Membership is layered. The two layers are independent: **Org membership** ([Members](/org/members)) determines who has access to the org as a whole — the dashboard, the list of sites, the subscription, and the org members screen. **Site membership** ([Site Members](/site/members)) determines who has access to a specific site's content, models, and builder. An org member can be added to any number of sites within that org. Conversely, a site member must be an org member first. Different roles can apply at each level. A user might be an org admin but only an editor on one specific site, or an org member with no site access at all (until they're invited to a specific site). For most teams the layering is invisible — everyone is an org member with access to every site. The split matters when you need finer control: agencies who want clients to see only their own site, contractors who should only access one site, or teams where the billing owner shouldn't have edit access to every project. → [Members](/org/members) → [Site Members](/site/members) --- ## Site Limits and Subscription The number of sites an org can hold is tied to its subscription plan. Reaching the limit prompts you to upgrade the plan or purchase additional site slots when you try to create a new site. Deleting a site frees up a slot. Cloned sites count the same as built-from-scratch sites — every site, regardless of origin, occupies one slot in the plan. → [Subscription](/org/subscription) --- ## Site Slugs and URLs Each site has a slug — a short identifier that appears in the SleekCMS admin URL when you're working in that site (e.g., `/<site-slug>/content`, `/<site-slug>/builder`). The slug is independent of the site's deployed URL — your deployed site can be at any custom domain you've configured at the deploy layer. Slugs are stable once assigned. If you need to rename a site for organizational reasons, change the display name in [Site Configuration](/site/config); the slug stays the same. --- ## Common Setups **Single site** — Most orgs start (and stay) with a single site. The org wrapper is mostly invisible — billing, members, that's it. **Multi-brand** — One org per company, one site per brand. Each brand has its own models, content team, and deployed domain. Shared billing simplifies vendor management. **Agency** — One org per agency, one site per client. Each client site is independent; the agency org sees them all. Clients can be granted site-level membership to their own site only (not the others). **Staging vs. production** — Use a single site with multiple [environments](/integrate/envs), not separate sites. Sites are independent content containers; staging and production are versioning concerns within a single site. --- ## What's Next - [Organization Management](/org) — The org overview. - [Members](/org/members) — Org-level membership management. - [Subscription](/org/subscription) — Plans and site limits. - [Templates](/org/templates) — The template gallery and clone tokens. - [Site Settings](/site) — Site-level configuration and team management. - [Getting Started](/getting-started) — Walkthrough of creating your first site. --- # Member Management Organization members are the people who have access to an org. The org member list controls who can see the org dashboard, browse sites, and be invited to specific sites. It's separate from site membership — joining an org doesn't automatically grant access to every site inside it. This page covers org-level roles, the invite flow, how org and site membership interact, and how to remove or disable members. --- ## Org-Level Roles There are three roles at the org level: **Owner** — Full control over the org, including billing. Every org has exactly one owner (the person who created it) by default, though ownership can be transferred. The owner is the only role that can manage the subscription, change the org's plan, and add/remove sites that exceed the plan limit. **Admin** — Can invite and manage members, manage sites, and access every site in the org. Cannot modify billing or transfer ownership. Most teams operate with one or two admins handling day-to-day org management. **Member** — Can see the org dashboard and access sites they've been explicitly granted access to. Cannot invite other members, manage sites, or see billing. This is the default role for new invites. The three roles map cleanly to "billing", "ops", and "user" — a small team typically has one owner, one or two admins, and everyone else as members. --- ## The Invite Flow To add someone to the org: 1. From the Members screen, click **Invite Member**. 2. Enter their email and pick an org-level role (member, admin, owner). 3. Optionally, immediately grant them access to specific sites with site-level roles. 4. Send the invite. The invitee receives an email with a link to accept. Until they accept, the invite shows up as **Invited** in the members list. After accepting, they appear as **Active**. You can invite people who already have a SleekCMS account (they accept the invite when signed in) or new users (they create an account when accepting). Either path lands them in the org with the role you assigned. --- ## How Org and Site Membership Interact Membership is two-layered. An org member becomes a site member when they're explicitly added to a site: - Org members **without** site membership see the org dashboard but cannot enter any site's content or builder. - Org members **with** site membership see the sites they've been granted access to. The site-level role (admin or editor) determines what they can do within that site. Org admins automatically have access to every site in the org with admin-level permissions. They don't need to be individually added to each site. Org owners have the same site access as org admins, plus billing access. The split is intentional — it lets you have org members who don't have access to any site yet (useful for staged onboarding, contractors waiting on a specific project, billing-only owners) and lets agencies grant clients access to a single site without exposing the rest of the org. → [Site Members](/site/members) → [Sites](/org/sites) --- ## Changing a Member's Role From the Members screen, the action menu next to each member offers role changes: - **Promote to admin / demote to member** — Adjust org-level role. - **Transfer ownership** — Owner-only action that moves the owner role to another member. The previous owner becomes an admin. Useful when the original creator leaves the team. Changing the role takes effect immediately. The member sees the updated permissions on their next page load. --- ## Removing Members To remove someone from the org: 1. Find them in the Members list. 2. Click the action menu and choose **Remove**. 3. Confirm. Removal: - Revokes their access to the org and every site within it. - Cancels any pending invites for that user in this org. - Leaves their account intact (they can still log in to SleekCMS, just not to this org). If you'd rather temporarily disable rather than permanently remove (e.g., a contractor between engagements), set their role to a no-access state or simply remove them; you can re-invite later with the same email. --- ## Pending and Disabled States Members appear in one of three states: | State | Meaning | |---|---| | **Active** | Accepted the invite, has access | | **Invited** | Sent an invite, hasn't accepted yet | | **Disabled** | Membership exists but is suspended — no access | Invited members keep their pending state until they accept the email link. If they don't accept within a reasonable time, you can resend or revoke the invite. Disabled members are a soft pause — useful for offboarding flows where you want to keep the membership record but block access. Re-enabling restores their previous permissions. --- ## Audit and Activity The Members screen shows when each member was added and their current state. For deeper audit needs (who did what, when), use the activity log inside each site's settings — org-level activity history is more limited. --- ## What's Next - [Organization Management](/org) — Overview of org structure. - [Sites](/org/sites) — Managing the sites members get access to. - [Subscription](/org/subscription) — Plan limits including how many members an org can hold. - [Site Members](/site/members) — Site-level membership and fine-grained access for editors. --- # Subscription and Billings Every organization has a subscription that determines what features are enabled, how many sites the org can hold, and how billing works. The subscription is managed at the org level by the owner — admins and members do not see the subscription screen. This page covers what plans control, the billing model, adding sites, and the relationship between subscription, sites, and members. --- ## What the Subscription Controls A subscription gates a few things at the org level: **Site count** — The maximum number of sites the org can hold. Different plans include different baselines; additional site slots can usually be purchased on top. **Member count** — The maximum number of org members. Higher tiers allow more. **Features** — Some capabilities — multi-environment publishing, advanced access controls, larger file/upload limits, custom integrations — are gated to higher tiers. The features available to your org are visible in the subscription screen. **Support level** — Standard email vs. priority support, depending on plan. When you bump into a limit (try to create a site when you're already at the cap, invite a member beyond the seat count), the CMS prompts you to upgrade or add capacity inline. --- ## Plans Plan details — tier names, pricing, feature inclusions — are listed in the subscription screen and at sleekcms.com. The general shape: - **Free / Starter** — Single-site sandbox for learning and prototyping. - **Standard** — Production-ready for one or a few sites. - **Team / Business** — Multi-site setups, more members, more features. - **Agency / Enterprise** — Large-scale multi-tenant agency use, custom integrations. Upgrading and downgrading is self-service from the subscription screen. Mid-cycle changes are pro-rated. --- ## Adding Site Slots If you need more sites than your plan includes but don't want to upgrade to a higher tier, most plans allow purchasing **additional site slots** independently. The Add Sites option in the subscription screen lets you buy capacity in small increments. Each slot adds one to your site cap and is billed alongside the base plan. This is the right path for orgs that need just a couple more sites without jumping to the next tier. Agencies with many small client sites typically buy bulk slots rather than upgrading to enterprise. --- ## Billing Billing runs on a monthly or annual cycle, chosen at subscription time. Annual billing typically includes a discount. The subscription screen shows: - **Current plan and cycle** — Plan tier, billing frequency, next billing date. - **Payment method** — Card on file, with the option to update. - **Invoice history** — Past invoices as downloadable PDFs. - **Stripe billing portal** — A link to the full Stripe-managed portal for payment method changes, tax info, and detailed invoice management. Payments are processed through Stripe. Invoices are issued for each billing cycle and emailed to the org owner. --- ## Who Sees the Subscription Screen Only the **org owner** sees the subscription screen and can make changes to the plan or billing details. Org admins can see that a subscription exists and what plan the org is on, but cannot modify it. Org members and site members see nothing about billing. If billing access needs to transfer (the original owner is leaving the team), use the **Transfer Ownership** action on the org Members screen to move the owner role to another member. The new owner gains subscription access immediately; the previous owner is demoted to admin. → [Members](/org/members) --- ## Plan Changes and Downgrades **Upgrading** — Takes effect immediately. The new plan's features and limits become available; your billing prorates for the remainder of the current cycle. **Downgrading** — Takes effect at the end of the current cycle. You keep the higher-tier features and limits until the cycle ends, then the downgrade applies. If your current site count or member count exceeds the new plan's limits, you'll need to reduce before the cycle ends — the downgrade fails until limits are within the new plan's bounds. **Canceling** — Same as downgrading: takes effect at the end of the current cycle. After cancellation, the org enters a read-only state where existing content and templates are preserved but new edits and new deploys are restricted. Re-subscribing restores full access. --- ## Failed Payments If a payment fails, the org enters a grace period. During the grace period: - A warning banner is shown in the CMS. - Notifications are sent to the org owner's email. - The Stripe billing portal becomes the primary way to update the payment method. If the grace period ends without payment, the org enters the same read-only state as a cancellation. Restoring access requires resolving the payment and re-subscribing. --- ## Multi-Org Billing Each org has its own subscription. If a user belongs to multiple orgs (e.g., they own one org and are a member of another), each org bills independently to its own owner. There is no centralized billing across orgs. For consolidated billing across multiple business units, consider whether you actually need multiple orgs or whether a single org with multiple sites would work. --- ## What's Next - [Organization Management](/org) — Overview of org structure. - [Sites](/org/sites) — Managing the sites your subscription covers. - [Members](/org/members) — Managing org members and the seat count. - [Templates](/org/templates) — Cloning sites without consuming extra capacity (cloned sites count toward your site cap). --- # Form Submissions Any `<form>` element in your site that includes a `data-sleekcms="<name>"` attribute is captured automatically — submissions stream into SleekCMS without any backend setup, no action URL, no JavaScript glue. The Forms section of the CMS is where those submissions land. This page covers what arrives in the inbox, how submissions are organized, and where the form schema is defined. --- ## How Submissions Are Captured When a visitor submits a form on your published site, the SleekCMS forms backend receives the request, validates the form name, and stores the submission against that form. Every named input on the form is persisted as a field on the submission record. ```html <form data-sleekcms="contact"> <input name="name" type="text" required> <input name="email" type="email" required> <textarea name="message"></textarea> <button type="submit">Send</button> </form> ``` The `data-sleekcms` value groups submissions by form — `"contact"`, `"newsletter"`, `"quote-request"`, etc. Standard HTML `name` attributes on inputs become the field keys. No action URL is required. No JavaScript. Submissions are captured server-side as part of the SleekCMS deployment. → [Form Backend](/builder/forms) --- ## The Submissions Inbox The Forms section in the SleekCMS sidebar lists every form your site has received submissions for. Selecting a form opens its inbox — a table of every submission grouped by form name, with the fields shown as columns. For each submission you see the time it was received, the values of every named field, and the user agent / referrer captured at submit time. You can scan, sort, and search across the table to find specific submissions. Submissions are read-only. They cannot be edited from the inbox — they're a record of what the visitor sent, preserved exactly as received. --- ## Form Schemas vs. Submissions It's useful to keep two concepts separate: **The form** lives in your site code — it's HTML markup with a `data-sleekcms="<name>"` attribute and a set of named inputs. You author the form in the Builder section (under [Form Backend](/builder/forms)) or directly in any template. **The submissions** live in the SleekCMS database — captured by the backend when visitors submit the form. They're viewed in the Forms section. The two are loosely coupled by the form name. Renaming the form (changing `data-sleekcms`) effectively starts a new submission group; the old submissions stay attached to the previous name. --- ## Access Control Submissions are visible to admins by default. Editors do not see the Forms section unless explicitly granted access through fine-grained permissions on their membership. → [Site Members](/site/members) --- ## What's Next - [Form Backend](/builder/forms) — Building forms in your site code and configuring the form processor. - [Site Members](/site/members) — Granting editors access to submissions. - [Content API](/integrate/api) — Reading site content programmatically (note: form submissions are not exposed via the content API). --- # Video Assets Video fields in SleekCMS reference hosted video content. Videos are uploaded to a provider — Bunny.net by default — and SleekCMS stores the resolved metadata: source, video id, embed URL, and title. Your templates and frontend code render the video through the `embed()` helper. This page covers how videos are added, the shape of video data, and how to render videos in templates and through the content API. --- ## How Video Fields Work A `video` field on a content model accepts a single video reference. Editors upload the file through the SleekCMS UI; the file is sent to the configured video provider (Bunny.net) and the resulting playback metadata is stored on the content record. Videos are not uploaded as raw files into SleekCMS storage. The CMS only stores the reference to the hosted video — provider, video id, embed URL, and title — so the streaming and bandwidth costs sit with a dedicated video CDN built for that workload. > **Do not write video field values by hand.** The `source`, `video_id`, and `embed_url` are issued by the video provider on upload. Hand-rolling them or guessing them produces broken embeds. Upload through the editor and let the sync engine record the resolved object. --- ## The Video Object A video field value resolves to an object with this shape: ```json { "source": "BUNNY", "video_id": "...", "embed_url": "https://iframe.mediadelivery.net/embed/...", "title": "..." } ``` | Property | Description | |---|---| | `source` | Identifier for the hosting provider (e.g., `"BUNNY"`). | | `video_id` | Provider-specific video identifier. | | `embed_url` | The iframe-ready embed URL provided by the host. | | `title` | The title set on the video during upload. | --- ## Rendering Videos in Templates Always render videos with the [`embed()`](/builder/code/context) helper. It produces a provider-aware `<iframe>` with the right attributes — including `allowfullscreen`, `allow="..."`, and dimensions — without you having to hand-craft markup. ```ejs <%- embed(item.intro_video) %> <%- embed(item.intro_video, '1280x720') %> <%- embed(item.intro_video, { autoplay: true, muted: true, loop: true }) %> <%- embed(item.intro_video, { size: '800x450', class: 'video', style: 'border-radius:8px' }) %> ``` The `attr` parameter accepts either a `"WxH"` string or an object with `w`, `h`, `size`, `autoplay`, `muted`, `loop`, `preload`, `t`, `class`, and `style`. Most browsers block autoplay unless the video is also muted — set both `autoplay: true` and `muted: true` together if you want videos to play automatically on page load. --- ## Videos via the Content API When fetching content through the API, video fields are returned as the same object shape stored in the content record: ```typescript const page = client.getPage('/about'); // page.intro_video → { // source: 'BUNNY', // video_id: 'a1b2c3d4', // embed_url: 'https://iframe.mediadelivery.net/embed/...', // title: 'Company intro' // } ``` Your frontend renders the video using `embed_url` directly in an `<iframe>`: ```html <iframe src="{{ video.embed_url }}" allowfullscreen allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" ></iframe> ``` For React, Vue, Svelte, or any other framework, the pattern is the same — drop the `embed_url` into an iframe and apply the layout styling your design calls for. --- ## What's Next - [Media Management](/media) — Overview of images, videos, and files in SleekCMS. - [Image Management](/media/images) — The image transformation and optimization system. - [Template Context and Data Access](/builder/code/context) — Full reference for the `embed()` helper and other template utilities. - [Content Field Types](/model/fields) — Other field types you can mix with video fields on a model. --- # File and Document Assets File fields hold downloadable documents — PDFs, spreadsheets, presentations, and other documents your visitors need to access. Editors upload the file through the SleekCMS UI; the CMS stores it on the files CDN and exposes a stable URL, display name, size, MIME type, and extension. This page covers what file fields store, the supported types and size limits, and how to render file links in templates and from the content API. --- ## How File Fields Work A `file` field on a content model accepts a single uploaded document. The editor's file picker uploads to SleekCMS storage and stores the resolved metadata on the content record. The original file is served as-is — no transformation, no compression. Documents are kept verbatim. > **Do not write file field values by hand.** The `url` and `name` are issued by the upload service. Hand-crafting them or pointing to external URLs is not supported in this field type — for external links, use a `link` field instead. --- ## The File Object A file field value resolves to an object with this shape: ```json { "name": "annual-report-2026.pdf", "url": "https://files.sleekcms.com/...", "size": 1843211, "type": "application/pdf", "ext": "pdf" } ``` | Property | Description | |---|---| | `name` | Display name of the uploaded file. Use this in download links. | | `url` | Download URL on the SleekCMS files CDN. | | `size` | File size in bytes. | | `type` | MIME type (e.g., `application/pdf`). | | `ext` | File extension without the leading dot (e.g., `pdf`). | --- ## Supported File Types File fields accept documents in the following formats: - **PDF** (`.pdf`) - **Spreadsheets** (`.csv`, `.tsv`, `.xls`, `.xlsx`) - **Word documents** (`.doc`, `.docx`) - **Presentations** (`.ppt`, `.pptx`) - **Plain text** (`.txt`) The maximum file size is **10 MB** per upload. Anything larger needs a different hosting approach — for video, use a [`video` field](/media/videos); for large archives or media, host externally and reference with a `link` field. --- ## Rendering File Links in Templates Use `item.<field>.url` as the link target and `item.<field>.name` as the display text. Add the `download` attribute if you want the browser to download the file rather than open it inline. ```ejs <% if (item.brochure) { %> <a href="<%= item.brochure.url %>" download> <%= item.brochure.name %> </a> <% } %> ``` For a richer download card with size and type: ```ejs <% if (item.report) { %> <a href="<%= item.report.url %>" download class="file-link"> <span class="file-name"><%= item.report.name %></span> <span class="file-meta"> <%= item.report.ext.toUpperCase() %> · <%= Math.round(item.report.size / 1024) %> KB </span> </a> <% } %> ``` --- ## Files via the Content API When fetching content through the API, file fields return the same object shape stored on the record: ```typescript const page = client.getPage('/resources/whitepaper'); // page.document → { // name: 'whitepaper.pdf', // url: 'https://files.sleekcms.com/...', // size: 2018342, // type: 'application/pdf', // ext: 'pdf' // } ``` Your frontend renders the link with `document.url` and `document.name`: ```tsx <a href={page.document.url} download>{page.document.name}</a> ``` --- ## What's Next - [Media Management](/media) — Overview of images, videos, and files. - [Image Management](/media/images) — Image transformations and CDN delivery. - [Video Assets](/media/videos) — Embedding hosted videos in content. - [Content Field Types](/model/fields) — Other field types available on content models. --- # Templates Every site in SleekCMS can be used as a template. A template is a complete, clonable site — content models, view templates, blocks, sample content, and configuration — wrapped behind a single **clone token**. Anyone with the token can spin up a new site that is an independent copy of the original. This page covers what a template includes, how clone tokens work, where to find and use templates inside the CMS, and how to share your own sites as templates. --- ## What a Template Includes When you clone a site from a template, the new site is created with everything needed to be functional from day one: - **Page models, entry models, block models** — The complete content schema. - **View templates and layouts** — All EJS templates bound to those models. - **Option sets** — Any dropdown taxonomies defined on the source site. - **Site builder assets** — CSS, JavaScript, images, and other static files. - **Sample content** — The page records, entries, and media that were present when the clone token was generated. - **Site configuration** — Builder settings, image processing, localization, and other site-level toggles. The clone is independent. Once created, edits to the original site do not propagate to the clone, and edits to the clone do not affect the original. They share no runtime state. --- ## The Template Gallery From your organization, the **Templates** view in the SleekCMS UI shows a curated list of ready-to-use starter sites — landing page templates, blogs, documentation sites, restaurant menus, portfolio sites, and more. Clicking any of these starts a new site cloned from that template. You can also paste a clone token directly to clone from any site you have a token for, including private sites that aren't in the public gallery. --- ## Clone Tokens A clone token is the credential that authorizes the cloning of a site. It is generated from the source site's settings screen (Site → Site Clone Token) and represents a snapshot of the site at the moment the token was created. Treat clone tokens like credentials. Anyone with the token can create a complete copy of your site. Tokens issued earlier do not reflect later edits to the source site — to share a fresher snapshot, generate a new token. → [Site Configuration](/site/config) — Where clone tokens are generated. --- ## Sharing Your Site as a Template Any site you own can be shared as a template by: 1. Polishing the source site so the structure, sample content, and templates are presentable. 2. Generating a clone token from the site's configuration screen. 3. Sharing that token with whoever should be able to clone the site. For agencies, this is the primary way to deliver pre-built site architectures to clients — build once, hand over a clone token, and the client gets their own independent copy they can edit freely. For internal teams, templates let you standardize new site setups. Create a "marketing site" template, a "campaign microsite" template, or a "blog" template, and use them as starting points whenever a new project kicks off. --- ## When to Clone vs. When to Reference Cloning produces a brand new site with its own content, models, and storage. Use this when: - You want a new project that starts from a known baseline. - You're handing a site off to a client who needs ownership. - You want to experiment freely without affecting the original. If instead you want **one site whose content shows up in many places**, that's a job for the [content API](/integrate/api), not a template clone. Templates are for spinning up new independent sites; the content API is for syndicating one source of content across multiple frontends. --- ## What's Next - [Sites](/org/sites) — Managing the sites that exist within your organization. - [Site Configuration](/site/config) — Generating clone tokens from a source site. - [Getting Started](/getting-started) — The full new-site workflow, including starting from a template.