# General concepts — What is DatoCMS?

Source [docs]: https://www.datocms.com/docs/general-concepts.md

DatoCMS is a cloud-based headless CMS designed to work with websites, mobile apps, and server-side applications of any kind. Freelancers, agencies, and startups use DatoCMS to empower non-technical clients and team members to manage the content of their digital products within a web-based CMS.

#### What does "headless CMS" mean?

A headless CMS clearly separates the actual content from the display layer and the front-end user experience.

The headless CMS concept stems from the demands of the digital era and a business’s need to engage customers with personalized content via multiple channels at all stages of the customer journey.

As the content in a headless CMS is considered *pure* (because it has no presentation layer attached), just one instance of it can be used for display on any device: websites (both desktop and mobile), apps, smartwatches, digital signage, Internet of Things devices, etc.

To learn more, you can read our [Introduction to Headless CMS](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md) over at the DatoCMS academy.

#### API-first

DatoCMS provides a content infrastructure that comprises different APIs for working with your content. Each of these APIs serves a different purpose, so which one to use depends on what you want to do:

-   To obtain content for presentation to users on a website or app, it is recommended that you utilize either the [Content Delivery API](/docs/content-delivery-api.md) or the [Real-time Updates API](/docs/real-time-updates-api.md). The latter is preferable if you require dynamic content that can be updated in real-time, delivering events as they happen.
-   If you want to programmatically create or update content items, or make any other change to your project/schema, use the [Content Management API](/docs/content-management-api.md).
    

#### One account, multiple projects

Once you sign up to DatoCMS and create your account, you'll be able to create an arbitrary number of different projects. For each project you'll be given an administrative area at a specific domain (i.e., `[my-project].admin.datocms.com`) from which you'll be able to invite collaborators to manage its specific content. All the projects you create will be completely isolated from each other.

### New to DatoCMS?

If you want to get started with DatoCMS and learn the basics, check out these video tutorials for beginners!

[

(Image content)

A gentle overview of all the features of DatoCMS

Play video »

](https://www.youtube.com/watch?v=ALHwdztg0UQ)

[

(Image content)

Creating a localized blog using Next.js

Play video »

](https://youtu.be/3tBeOwdVuwo)

[

(Image content)

Next.js + DatoCMS tutorial for beginners

Play video »

](https://www.youtube.com/watch?v=_VIF1if-dNA)

---

# General concepts — Workspaces: Organizations and Personal Accounts

Source [docs]: https://www.datocms.com/docs/general-concepts/organizations-and-accounts.md

When you only have a single DatoCMS project, it's simple to organize: just one project under one account. But when teams grow larger, they may need more powerful organizational tools. DatoCMS provides two kinds of workspaces (project groupings):

(Image content)

Workspaces and Projects

### Personal Accounts

**Personal account** workspaces are designed for individual use. This is the default kind of workspace when you create a new project under a personal account.

In this kind of workspace, projects belong to a single account (your DatoCMS login), and other people can participate only as **Project Collaborators** that you invite inside the project itself. This is the right choice when someone simply needs to work on the content or configuration of a specific project.

### Organizations

**Organization** workspaces are built for teams who need to share billing, account management, and workspace-level settings, not just work on projects together.

In an organizational workspace:

-   **Project Collaborators** are invited into specific projects, with specific [per-project roles and permissions](/docs/general-concepts/roles-and-permission-system.md)
-   **Organization Members** belong to the workspace itself, outside of any projects, and their workspace-level permissions control access to billing, account settings, and organization-level management. These permissions are detailed on the rest of this page.
    

A person can be both a **Collaborator** on a project and an **Organization Member** of the workspace, and the two roles are independent. A person's account can belong to any number of projects as a collaborator, and to any number of organizations as a member.  
  

> [!PROTIP] Pro tip: Projects have Collaborators and Organizations have Members
> Remember:
> 
> -   **Collaborator** roles determine what a person can see and edit inside a specific **project**.
>     
> -   **Organization Member** roles determine what a person can see and edit inside the entire **organization workspace**.

## Creating an organization

You can create an organization by clicking on the scope selector in the top left of the nav bar. When you create a new organization, you will be prompted to give that team a name. This is the name your organization will have on your dashboard, and what other members will use to access your organization.

(Video content)

##### Converting a Personal Account into a new organization

At any time, you can move all projects, your plan, credit card and billing information linked to your personal account to a new organization. This can be especially useful for all those personal accounts created before organizations were available in DatoCMS.

To convert your personal account into an organization, go to the **Edit Account** tab, and select the option "Move projects and billing information to a new organization".

(Video content)

Your personal account will be switched to a free Developer plan, and the new organization will hold all of your existing projects and billing information.

You will also become the first owner of the newly created organization.

## Organization member permissions: Owners and Viewers

When you invite someone to a DatoCMS organization, they become a **Member**. Each member can be an **Owner** or **Viewer:**

### **Organization Owners**

**Owners** have full privileges to the organization and all of its projects. An owner can manage billing, change the subscription plan, invite other members to the organization, manage all projects' settings, and **automatically enter any organization-owned project with full privileges** (even without an explicit Collaborator seat inside the project). They are also the only members who can delete the organization.

> [!POSITIVE] Organizations can have more than one owner!
> For the sake of ownership continuity for your organization, it would ideally have at least two members with owner permissions (e.g., perhaps a manager and a billing person). This helps ensure your DatoCMS organization will continue to be accessible in the event of turnover.
> 
> Adding additional owners will not remove any existing ownership. Each owner has their own login and can independently access and modify the organization as necessary.

### **Organization Viewers**

**Viewers** can only view organization settings. They cannot change them. This role can be useful for people on your team who deal with finance/invoices/administration, so that they can download invoices and track costs without making accidental changes to existing projects or the organization itself.

Unlike Owners, Viewers **do not have automatic, implicit access to the organization's projects**. They can only enter projects if they have an explicitly defined Collaborator seat inside those specific projects that grant them access.

### Table of Owner & Viewer Permissions

The table below summarizes the permissions for each type of member:

| Permission | Owner | Viewer |
| --- | --- | --- |
| Manage members/roles | ✅ Yes | 👁️ View-only Can see other org members, but not add/remove/change them. |
| Manage plan and billing | ✅ Yes | 👁️ View-only Can view and download invoices, but not edit billing. |
| Transfer projects | ✅ Yes | ❌ No |
| Rename/delete organization | ✅ Yes | ❌ No |
| Create/edit/delete projects | ✅ Yes | ❌ No |
| Enter the organization's projects | ✅ Automatic full access Has automatic, implicit, fully privileged access to all projects owned by the organization — even without an explicit Collaborator seat in the project. | ❌ No automatic access Does NOT have any implicit access via the org. Can only enter project if they have a separate Collaborator seat inside the project. |

### Organizational email notifications

Organization **owners** will also receive email notifications for important events, like account payment issues and subscription deactivations/reactivations. Org **viewers** and project **collaborators** will *not* receive these emails.

Please see details at: [Payment failures and billing notifications](/docs/plans-pricing-and-billing/payment-failures-and-billing-notifications.md)

> [!POSITIVE] Partner specific roles
> Organizations that have been accepted through our [partner program](https://www.datocms.com/partner-program.md) have access to additional roles to manage their projects across clients, as you can check out [here](/docs/agency-partner-program/partners-dashboard.md#developer-and-projects-manager-roles)

### Inviting new members

To invite new members to your organization, select the organization from the scope selector, then open the **Members** tab. Enter the email address of the person you would like to invite, select their role, and click the "Invite" button.

(Video content)

As the organization Owner, you can add new members, remove existing members, and change their roles. Members who have accepted an invitation to the team will be displayed as members with their assigned roles.

### Different ways to enter a project: Project owners, Organization Members, and Collaborators

Let's recap the ways in which one can enter a DatoCMS project:

##### If the project is inside a personal account

-   The owner account always enters the project with full privileges, **even if they are not explicitly listed as a collaborator**. They have implicit admin permissions due to their ownership.
-   Accounts invited as [collaborators](/docs/general-concepts/roles-and-permission-system.md) within the project enter with the permissions of the specific [role](/docs/general-concepts/roles-and-permission-system.md) they have been given.
    

##### If the project is inside an organization

-   Members of the organization with the Owner roleenter the project with full privileges.
-   Accounts invited as [collaborators](/docs/general-concepts/roles-and-permission-system.md) within the project, enter with the permissions of the specific [role](/docs/general-concepts/roles-and-permission-system.md) they have been assigned.
    

> [!WARNING] Collaborators take precedence over organization memberships
> Within an organization, there is a further possibility worth emphasizing: if a member of the organization has also been invited as a collaborator, **the role as collaborator takes precedence**: they do not enter with full privileges, but with the permissions of the specific role they have been assigned as collaborator.

Take, for instance, a case in which someone is responsible for billing matters but will not interact with content. In this case, it would be reasonable for them to be an organization owner (so as to modify billing information) but to also be invited to the project as collaborator using a role with few privileges — probably read-only.

Also consider a user who is totally in charge of a marketing website, but should not have power within the organization itself. This user should be invited as collaborator with full privileges to the project, but should be a simple Viewer at the organization level, if they are even to belong to the organization at all.

### Leaving an organization

To leave an organization, select the organization from the scope selector, then open the Members tab. Find your account in the list of members, and then press the "Leave the organization" button.

(Video content)

> [!WARNING] At least one member must be owner!
> You can't leave an organization if you are the last remaining owner. To leave an organization, first assign the owner role to at least one organization member.
> 
> If you are the only remaining member, you should delete the team instead.

### Forgotten Password?

If you forgot the password to your DatoCMS account, you can easily [reset it](https://dashboard.datocms.com/forgot-password). You will receive an email reset link to the email associated with your login.

### Losing Access to Your Two-Factor Authentication

If you find yourself unable to access your account due to issues with two-factor authentication (2FA), there are immediate steps you can take to regain access.

You have two options:

1.  Locate your One-Time Password (OTP) backup codes that were provided at the time you set up 2FA. These codes can be used in place of the 2FA code to log into your account.
    
2.  If you have lost your OTP backup codes as well, or have used them all, please [contact our support team](https://www.datocms.com/support.md?topics=account-access/account-login-and-recovery) for assistance.
    

#### Asking your organization owners for a 2FA reset

Alternatively, if you do not have any personal projects and your account belongs to only one organization (meaning you are either a member of the organization or a collaborator on one of its projects), you may request a 2FA reset through your organization.

To start this process, at the Authentication Code prompt, click on "Lost access to two-factor authentication." Then, select "Request 2FA reset."

(Video content)

The owners of your organization will be notified by email and within the Dashboard, where they can either accept or refuse your request.

(Video content)

After one of the organization's owners approves your request, you will be notified via email that 2FA has been disabled on your account . This will allow you to log back into the Dashboard and set up 2FA from scratch for projects that require it.

(Video content)

---

# General concepts — Project collaborators, roles and permissions

Source [docs]: https://www.datocms.com/docs/general-concepts/roles-and-permission-system.md

> [!PROTIP] Pro tip: Project Collaborators vs Organization Members
> There are two main ways to invite another user to work on a project with you:
> 
> -   If they only need to work on **content** inside your CMS, you're on the right page! Invite them as a Collaborator (see below).
>     
> -   But if you want them to be able to see or change **billing and account information**,[invite them as an **Organization Member**](/docs/general-concepts/organizations-and-accounts.md) instead.
>     
> 
> Someone can be both a Collaborator and an Organization Member, and that's OK. Their project Collaborator permissions determine what content they can edit, and their Organization Member permissions determine what billing/account info they can see or edit.
> 
> **Remember: Collaborators determine project permissions. Organization Membership determines account permissions.**

In addition to the [account(s) owner of the project](/docs/general-concepts/organizations-and-accounts.md#entering-a-project-project-owners-vs-collaborators), who always enters the project with full privileges, it is also possible to invite further users to a project, giving them more refined permissions.

These additional users in DatoCMS are called **Collaborators**, and for them DatoCMS offers a thorough roles and permissions system to precisely specify what actions they can perform (ie., “read-only permission on every content, except for articles which can be freely created/updated but cannot be deleted or published online”).

(Image content)

The permissions given to a collaborator is managed separately and independently in each DatoCMS project: ie. Jack can have full privileges in project A, but can be just a proofreader in project B.

### Default roles

Every DatoCMS project is automatically populated with the following roles, but you are free to create as many roles as you want, and assign them both to collaborators and API tokens:

-   **Admin:** Can do everything, including work with records, create and update models, configure project settings and work with API keys.
-   **Editor:** Can work with records, does not have access to models, API keys or project settings.
    

For each role, you can specify what the user is allowed and not allowed to do.

### Project-wide permissions

Roles can grant/deny the ability to access and configure the project's administrative settings, including:

-   Models, fields and navigation bar;
-   Project's languages, deployment, time zones and SSO settings;
    
-   Roles and invite/remove collaborators;
-   Webhooks;
    
-   API tokens;
-   Shared filters.
    

(Image content)

### Control access to environments

Roles specify *access-level permissions* to [environments](/docs/general-concepts/primary-and-sandbox-environments.md). You can allow users to access:

-   All environments (useful for developers)
-   Only the primary environment (useful for content editors)
    
-   Only the sandbox environments (mostly useful for API tokens used in CI systems)
    

(Image content)

This setting is very useful because it allows you to pre-configure the content-level permissions (which records a user can create/update/delete/etc.) on sandbox environments without letting editors actually enter the environment and make changes until it gets [promoted to primary](/docs/general-concepts/primary-and-sandbox-environments.md#promotion-of-sandbox-environments).

In other words, the permission to access the environments takes precedence over the content-level permissions set inside a specific environment!

### Content-level permissions

For each environment, you can specify different permissions on actions that can be performed on records. Rules can be additive or subtractive, and are defined by:

-   **The action:**
    
    -   View
        
    -   Create/duplicate
        
    -   Edit
        
    -   Publish/unpublish
        
    -   Delete
        
    -   Take over
        
    -   Move to stage (for [workflows](/docs/general-concepts/workflows.md))
        
-   **The model:** i.e., it's possible to give full access (everything allowed) to the model `meal`, but give zero access (can't even read) to the model `drink`.
-   **The creator:** i.e., it's possible to edit only the content which the user has created themselves (or users with its same role), and deny opening content created by other users.
    

The most important aspect is that **everything which is not explicitly allowed is denied**. Here's an example: if you've granted a user the permission to edit some records, you also need to give them permissions to *view* them, or they won't be able to open the record and make the changes.

Even though it might feel counter-intuitive, this way of handling access rights helps to prevent unsolicited access: when you set up everything explicitly, there is no chance of accidentally giving someone access to something they shouldn't have.

#### Translator role, and locales permissions

On an Enterprise plan, your project and models can have locale-specific permissions. For each translator role, you can define the specific locale(s) they have access to, limiting their ability to view/add/edit/remove records to just those languages. This can be applied either globally across your whole project, or more granularly defined on a per-model basis.

Need locale-specific permissions? [Talk to our Sales team about an Enterprise plan](https://www.datocms.com/contact.md).

(Image content)

Every role can customize which locales can be edited

#### Workflows permissions

If some of your models are under a [Workflow](/docs/general-concepts/workflows.md), you can also define which actions are allowed depending on the specific stage a record is in. You can learn more in the [appropriate section](/docs/general-concepts/workflows.md#how-to-configure-workflows) of the documentation.

#### Forking the environments

When [forking an environment](/docs/general-concepts/primary-and-sandbox-environments.md#creating-a-new-sandbox-environment), for each existing role, DatoCMS duplicates the content-level permissions you had on the original environment to the copy.

### Asset permissions

Together with the actions that you can perform on the records, similarly you can apply the following permissions on assets:

-   **The action:**
    
    -   View
        
    -   Create/duplicate
        
    -   Edit metadata/replace asset
        
    -   Delete
        
    -   Edit creator
        
-   **The creator:** i.e., it's possible to edit only the assets which the user has created themselves (or users with its same role), and deny using assets created by other users.
    

### Asset Collection permissions

If you're using Asset Collections, you can also assign specific permissions for users to those, including read, write, and a new dedicated `move` permission that controls whether users can move assets from one collection to another.

(Video content)

Permissions assigned to a collection are automatically inherited by its sub-collections, with inheritance rules clearly displayed in the UI.

## Putting everything together with an example

To better exemplify, let’s consider a project that:

-   has only one environment (the primary one), and
-   has a *Blog Editor* role that:
    
    -   **Access-level permission:**only gives access to the primary environment, and...
        
    -   **Content-level permission**: …only allows to manage records of type `article`.
        

If I fork the primary environment to a new one called `foobar`, the content-level permissions get duplicated (that is, blog editors can only manage records of type `article` also inside the `foobar` environment). **But** **they will only be able to do so when the** **`foobar`** **environment gets promoted to primary**, since the access-level permission doesn't give them access to sandbox environments.

This allows to safely test and experiment changes to permissions on the roles without affecting the work of your collaborators on the primary environment, and without giving them privileges to edit sandbox environments.

## Roles inheritance

As your project grows and evolves, permissions on each role can become quite complex. To allow greater modularity, simplicity and clarity, you can organize your roles in hierarchies. In this way, users with greater rights automatically inherit all the permissions of roles lower in the hierarchy, without having to duplicate permissions assignments for multiple different roles.

(Image content)

An example of what you can accomplish with inherited roles

You can configure the hierarchy using the “Inherits permissions from” field in the Edit role form:

(Image content)

Every role can specify the list of roles to inherit permissions from

#### Learn more about roles and permissions

Get a hands-on learning experience with our tutorial videos:

[

(Image content)

Intro to Settings & Configurations

Play video »

](https://www.datocms.com/user-guides/the-basics/intro-to-settings-configurations.md)

[

(Image content)

Using Roles for Content Governance

Play video »

](https://youtu.be/_5bwi9SsGss)

---

# General concepts — The content schema

Source [docs]: https://www.datocms.com/docs/general-concepts/data-modelling.md

DatoCMS can be seen as an editor-friendly interface on top of a database, so the first step is to build the actual schema upon which users will generate the website content.

#### Models

The way you define the kind of content you can edit inside each different administrative area involves the concept of models, which are like database tables.

Each administrative area can specify a number of different models, and they represent blueprints upon which users will store the website content.

For example, a website project can define different models for articles, products, categories, and so on.

#### Fields

Each model consists of a set of fields that you define (strings, numbers, uploads, videos, relationships between objects). Each field has a name and additional metadata, such as validations or particular configurations to better present the field to the editor.

Fields in DatoCMS can also be localized if you need to accept different values based on language.

#### Records

DatoCMS stores the individual pieces of content you create from a model as records, which are like table rows in a database.

[

(Image content)

Intro to the Schema Builder

Play video »

](https://www.datocms.com/user-guides/the-basics/intro-to-the-schema-builder.md)

[

(Image content)

Intro to Models in DatoCMS

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-models-in-datocms.md)

[

(Image content)

Intro to Fields in DatoCMS

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-fields-in-datocms.md)

---

# General concepts — Organizing content

Source [docs]: https://www.datocms.com/docs/general-concepts/navigation-bar.md

Every time a new model is created, a new menu item is automatically added to the **Content** tab's sidebar, so that editors can start creating new content right away:

(Image content)

While this is great, big websites tend to require a significant number of models to properly manage every page, so it might quickly become difficult for clients/editors to understand which model in the backend is linked to which part of the frontend website.

You can easily organize the different models in a more understandable way by renaming, reordering, and grouping them, so that their purpose will be clearer to editors:

(Video content)

Something else you can consider is using menu items that point to external URLs.

This allows you to link to third-party resources, but also to create custom links to special records or filtered sets of records for fast retrieval.

---

# General concepts — Record versioning

Source [docs]: https://www.datocms.com/docs/general-concepts/versioning.md

DatoCMS produces a snapshot of a record each time it gets saved.

Record versioning allows DatoCMS users to view previously published versions of the record, find out who published a record, compare previous snapshots to the current version, and — when necessary — restore the content to the earlier state.

(Video content)

DatoCMS stores all the content found in the record — including localized content and references to other records and uploads. However, it does not create or store snapshots of linked entities. Therefore, if you restore a record to the earlier version containing a reference to a deleted upload, the image field will be empty.

It is also important to remember that the version comparison only displays current locales and values. If your record was translated into Italian in the past, but later the Italian locale was removed from the model, the Italian text will no longer be visible or restorable.

The same logic goes for deleted fields: any content that was stored within these fields in the past will no longer be displayed.

To know more about how versioning on DatoCMS works, check out this video tutorial:

[

(Image content)

Content Records, Publishing, Scheduling, and Versioning

Play video »

](https://www.datocms.com/user-guides/content-management/content-records-publishing-scheduling-and-versioning.md)

[

(Image content)

Working with entry & asset versions

Play video »

](https://youtu.be/qJhobECFQYk)

## How long do record versions last?

Record history retention depends on your plan. As of 2025, the history limits are:

-   3 days on the Free plan
-   60 days on the current Professional plan
    
-   Enterprise plans and older, grandfathered plans have custom limits
    

After this period, only the latest version will remain.

---

# General concepts — Draft/published system

Source [docs]: https://www.datocms.com/docs/general-concepts/draft-published.md

You can decide to activate the draft/published system on a per-model basis:

(Video content)

If you do so:

-   When you create a new record, it will be put into a *Draft* status. This means that the record is still not published: you can continue making changes and saving the record without having to worry about showing unfinished content to your end users.
-   Once you're satisfied with the changes, you can click on the *Publish* button: the latest revision of your record will be marked as the *Published version*, and it will be instantly available in the DatoCMS APIs:
    
    -   With the [Content Delivery API](/docs/content-delivery-api.md) and the [Realtime Updates API](/docs/real-time-updates-api.md), the default is to return only the published record, but you can request to consider the draft with the header [`X-Include-Drafts: true`](/docs/content-delivery-api/api-endpoints.md#preview-mode-to-retrieve-draft-contenthttps://www.datocms.com/docs/content-delivery-api/api-endpoints#preview-mode-to-retrieve-draft-content).
        
    -   With the [Content Management API](/docs/content-management-api.md), you can request to consider the published or draft versions of records with the parameter [`?version=current`](/docs/content-management-api/resources/item/instances.md) or [`?version=published`](/docs/content-management-api/resources/item/instances.md).
        
-   If you make a change to a published record, its status will be become **Updated**. Again, those changes won't be visible to end users and published until you explicitly click on the *Publish* button again.
    

> [!POSITIVE]
> For more information on how the system manages the draft/published status, you can refer to this in-depth guide: [Data consistency: key concepts and implications](/docs/content-modelling/data-migration.md).

### Saving Invalid Drafts

In some instances you may need to create posts via the UI or the API that may not have all validations in place (for instance, bulk creating records missing a specific required field like a title).

In these cases, if you have the Draft/Published flow enabled, you can also choose to allow saving records on a draft stage without passing all validations.

The feature affects the CMS and, of course, the CMA (Content Management API). When draft saving is active, it's possible to POST/PUT invalid records to CMA and have them saved: the endpoints respond with a 200, and the record just saved as a payload.

(Video content)

However, validations will take effect when the record is published. If the record is not valid, publication fails, and editors need to fix the content to ensure all rules are handled before proceeding to move the record into the Published stage.

### Linked records must be published together

When a record links to other records, they all have to be published *together* — a published record cannot link to an unpublished draft, or your visitors would just see a broken link! Our system prevents this.

**For example:**

-   You have an article, "🟠 **Easiest Headless CMS**". This is the one you've edited and want to publish.
-   But it links to another article, "⚪ **CMS Comparison**". This is currently a draft (never published).
    
-   If you try to publish 🟠 **Easiest Headless CMS** without first publishing ⚪ **CMS Comparison**, the publish will fail.
    

> [!NOTE] What do the colored dots mean?
> In the CMS, a colored dot before the record name shows its current status:
> 
> **🟢** Green\= Published  
> 🟠Orange = Updated (changed since the previous publish)  
> ⚪ Gray = Draft (never published)

To resolve this, you can tell our system which behavior you prefer: whether we should fail such attempts, or try to publish the linked records ourselves.

In the field's settings, under its Validations tab, there is an option:

**"When a publishing is requested and this field references some unpublished records:"**

-   "**Fail the operation and notify the user**": The publish will be stopped and you'll see an error (or an email, in the case of a [scheduled publish](/docs/general-concepts/scheduled-publishing-unpublishing.md)). In our example, that means neither article will be published. ⚪ **CMS Comparison** will stay a unpublished draft and 🟠 **Easiest Headless CMS** will still show its older published version, not your latest changes.
-   "**Publish also the referenced records**": Our system will try to automatically publish all the linked-to records before publishing the linking-from record. In the example, that means it will automatically try to publish ⚪ **CMS Comparison** first, and then publish 🟠 **Easiest Headless CMS.** Both will be published at the end (assuming every linked-to record successfully published).
    
    Keep in mind that every linked-to record might, itself, have additional links and validations, subject to the same criteria. The entire "tree" of references (every linked record, and every record THEY link to) must successfully publish or the overall operation will fail — if *any* linked record *would fail* a publish, then the entire attempt is aborted and nothing is published. (In software developer terms, the entire chain of publish operations is *atomic* — it all happens together or not at all.)
    

(Video content)

## Video tutorial

To learn more about how DatoCMS saves versions, check out this video tutorial:

[

(Image content)

Working with entry & asset versions

Play video »

](https://youtu.be/qJhobECFQYk)

---

# General concepts — Scheduled publishing

Source [docs]: https://www.datocms.com/docs/general-concepts/scheduled-publishing-unpublishing.md

Combined with the [draft/published system](/docs/general-concepts/draft-published.md), you can schedule **future publications or unpublications**.

You can access this feature using the calendar icon in the dropdown menu for the "Publish" button:

(Image content)

This will automatically change the state of your record on the specified date.

## Linked records and scheduled publishing

When you schedule a record for publishing, and it has links to other records, those linked records must also be published at the scheduled time. You can either do this manually (publishing the linked-to records beforehand), or have our system try to automatically publish all the linked-to records at the scheduled time.

To learn more, please see: [Draft/published system: Linked records must be published together](/docs/general-concepts/draft-published.md#linked-records-must-be-published-together)

## Scheduling build triggers after publications/unpublications

If you are using build triggers, you can **set them to automatically trigger a build** when the scheduled publication/unpublication is done.

You can find this setting in the build trigger settings:

(Image content)

---

# General concepts — Media Area

Source [docs]: https://www.datocms.com/docs/general-concepts/media-area.md

In the Media Area of your project you can upload, view, edit, and organize all your assets.

(Image content)

Individual assets can be viewed with their information and edited.

(Image content)

### Metadata and smart tags

When an image is uploaded, it is analyzed, then a set of metadata is exposed in our media area and via the APIs. If the upload is a picture with EXIF info, we expose that information together with other details, such as dominant colors and a set of machine-learning generated smart tags.

(Video content)

Look at all this juicy data!

### Asset organization

As your collection of assets grows, organization becomes crucial. To help with this, we offer several options.

You can filter assets using any number of fields with various options for each field:

(Image content)

If you have a useful filter that you want to save or share with the rest of the team, you can add it to your "Saved filters":

(Image content)

One way to organize assets that we recommend is to **combine filters with tags**, both manual and smart tags, automatically added on asset upload.

You can efficiently tag assets using the bulk tagging feature:

(Image content)

DatoCMS also has *asset collections*, in addition to tags. You can create multiple collections, organize them in a tree structure, very much like folders on a classical file system. There are two main rules: each asset can only be assigned to one collection, and you can always view all assets by clicking on "All assets."

To create collections and nested sub-collections, simply utilize the sidebar as you would with content views.

(Video content)

Assets can be assigned to their respective collection or sub-collection using the action bar at the bottom of the screen or via drag and drop.

(Video content)

Finally, you can visualize your assets in various ways (grid, masonry, and table), depending on your use case and asset type. For example, the tabular mode can be very handy for performing operations on multiple assets at once:

(Image content)

### Asset management

For each asset, you can specify a set of default metadata such as title and alternate text that can be applied as the default value when nothing else is selected.

(Image content)

For better asset organization you can specify some additional categorization fields, such as notes for colleagues and author/copyright data of the asset:

(Image content)

If you need to add a new revision of an asset, you can simply drag in a new version, and we'll replace the asset in every occurrence:

(Image content)

### Localization

When using multiple locales, you can set default metadata on a per-locale basis:

(Image content)

You can then override the default metadata in place when referencing the asset in a record:

(Image content)

### Audio player

If you host/produce audio files, you can use the embedded audio player to listen to them:

(Image content)

### Image editor

If you need to edit an uploaded image, you can use the built-in powerful editor to crop, rotate, apply predefined color filters, tweak colors, and add basic shapes and text to the image:

(Video content)

### Image URL

When an asset is uploaded to your media area, you immediately get access to a direct URL to use it wherever you want:

(Image content)

To understand how that URL is formatted, we first need to understand how the file name is formatted after upload:

-   Underscores or dashes at the beginning or end of the file name are removed
-   Character accents are removed
    
-   All non-alphanumeric characters, except for underscores "\_", are replaced by dashes "-"
-   If the file has a wrong or invalid extension in its name, it is replaced by the one matching the file type
    

The URL then is created using the project ID, an upload timestamp, and the newly formatted file name:

```plaintext
https://www.datocms-assets.com/PROJECT_ID/UPLOAD_TIMESTAMP-FORMATED_NAME
```

### Video editor

If you need to edit an uploaded video, you can use the built-in powerful editor to trim, resize, rotate, apply predefined color filters, tweak colors, and make basic changes to the video:

(Video content)

When serving videos, we recommend using HLS streaming whenever possible. Follow our [docs on serving videos through Mux](/docs/content-delivery-api/images-and-videos.md#videos) to implement our recommended best practices.

To have an overview on all the things you can do in your media area, check out these video tutorials:

[

(Image content)

Intro to the Asset Area

Play video »

](https://www.datocms.com/user-guides/the-basics/intro-to-the-asset-area.md)

[

(Image content)

Images and Image Optimization

Play video »

](https://www.datocms.com/user-guides/media-management/images-and-image-optimization.md)

[

(Image content)

Videos and Video Optimizations

Play video »

](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)

### Antivirus Scanning

Every file uploaded to the Media Area is automatically scanned for viruses and malware. Scans run in the background immediately after upload so that editors don't need to do anything, and there's no delay in their workflow.

(Video content)

**How it works**

When a file is uploaded, a scan is queued automatically. Within seconds, the file is assigned one of the following statuses:

| Status | What it means |
| --- | --- |
| Clean | No threats detected and the file is served normally. |
| Infected | A threat was detected and the file is automatically quarantined and removed from the CDN. |
| Skipped | The file exceeds the scanner's size or type limits and couldn't be assessed. DatoCMS cannot confirm whether these files are safe. |
| Failed | A transient error occurred. The scan will be retried automatically up to 6 times and if all retries fail, the file remains in a failed state. |

If an asset is replaced with a new version, the scan runs again automatically on the new file.

**How infected files are handled**

When a threat is detected, DatoCMS automatically:

1.  Removes the file from public storage
    
2.  Purges it from the CDN cache, and
    
3.  Keeps the upload record visible in the Media Area, so editors can see it was flagged
    

The file URL will no longer serve any content. At this time, there is no way to restore a quarantined file, and editors should replace the asset.

> [!NOTE] Using a custom storage bucket?
> If your project uses a custom storage bucket like S3 or R2, DatoCMS doesn't have permission to delete or move files from your storage. The file will remain accessible from your bucket even after being flagged as infected. The upload record will be marked accordingly, and the warning screen will display the file path so you can remove it manually from your storage provider.

Infected files are surfaced throughout the Media Area:

A **"Threat detected"** badge appears on the upload card in grid, masonry, and table views. On smaller cards, this collapses to an icon with a tooltip

(Image content)

Opening an infected file replaces the normal preview with a **warning screen** that explains the situation, shows the specific threat name (useful for investigation), and prompts the editor to replace the asset

(Image content)

For custom storage projects, the warning is adjusted to show the file path and advise manual removal from the bucket

Editors can filter uploads by antivirus status (clean, infected, skipped, failed, pending) directly in the Media Area search. This filter is **not available** in the Content Delivery API.

Scan results are delivered in real time with the antivirus status in the dashboard updating live without requiring a page refresh, and Webhooks are fired on status changes, so you can build integrations that react to scan results, for example, getting a Slack alert when an infected file is detected in your project.

**API Access**

The antivirus status is also available on every upload object via the CMA, under a new `meta.antivirus` field:

```json
"meta": {
  "antivirus": {
    "status": "infected",
    "scanned_at": "2026-03-27T18:51:00Z",
    "threat_name": "Trojan.GenericKD.12345"
  }
}
```

The `status` field will be one of `clean`, `infected`, `skipped`, or `failed`. The `threat_name` field is only present when a threat has been detected.

Antivirus scan **results are preserved when forking environments** without any rescanning required.

When duplicating projects, **infected files are automatically excluded** from the copied project to prevent propagation.

---

# General concepts — Localization

Source [docs]: https://www.datocms.com/docs/general-concepts/localization.md

Each administrative area in DatoCMS supports multiple locales, which are defined by the short ISO locale codes (i.e. `en` or `de`). You can add or remove locales within the *Admin area \> Site settings* section:

(Video content)

## Field-specific localization

Each field is localized individually, so you can pick and choose which specific content needs to be translated and which does not:

(Video content)

As soon as a localized field is present within a model, the form to edit its records will present one tab for each locale:

(Image content)

## Adding new locales along the way

With DatoCMS you are free to add new locales at any time; just be aware that, once a new locale is added, if some validations are present on your fields, those validations will be enforced for every locale. Records already created will therefore be marked as “invalid”, and you won't be able to update your records until all the validations are satisfied for all the locales. For more information, take a look at the [Data migration](/docs/content-modelling/data-migration.md) chapter.

> [!PROTIP] Pro tip: Build a multi-language website with Next.js
> Our blog has a full walkthrough on how to set up a multi-language site from scratch using Next.js, which provides robust built-in support for internationalization.

## Optional/required locales

You can configure a certain model so that your editors are not forced to insert content for every language your project supports, but just for some of them, on a per-record basis.

This allows use cases such as multi-language blogs, where some articles can be written only in English, other only in Italian and others in both languages.

To require all locales to be always present on every record of a specific model, you can check the *All locales required?* option in your model settings:

(Video content)

## Fallback locales

If you allow partial (optional) translations, some records will inevitably have gaps: a locale where only a few fields have been filled in, or a page complete except for one field.

A **fallback locale** is a safety net for these situations: a backup locale that your website can show whenever a piece of content (a specific field) is missing in the locale a visitor asked for.

Fallback locales work best between languages that are similar to each other, like regional dialects or variants of the same base language. Say your online store uses three locales: **English** (`en`) as the base, plus **English (US)** (`en-US`) and **English (UK)** (`en-GB`). You write each product page once in base English, and only override the handful of fields that actually differ by region. For a wool sweater, that might look like this:

| Field | Base language (English, `en`) | 🇺🇸 United States (`en-US`) | 🇬🇧 United Kingdom (`en-UK`) |
| --- | --- | --- | --- |
| Product name | Wool Sweater |  | Wool Jumper |
| Description | Knitted from 100% merino wool, this classic crew neck keeps you warm without the bulk. Naturally breathable, machine washable, and made to last for years. |  |  |
| Shipping | Available globally; fees vary | Free shipping in 3-4 business days (AK/HI extra) | Free UK delivery in 2–3 working days |

With base English configured as the fallback:

-   🇺🇸 US visitors see the fallback product name and description, but with US-specific shipping details
-   🇬🇧 UK visitors see their own product name ("jumper" instead of "sweater") and different shipping details. They see the same fallback description
    

Fallbacks can also be a *chain* of multiple locales. If our example store later adds **English (Australia)**, its fallback chain might be configured to try **English (Australia)** first, then **English (UK)** next (whose spelling and conventions are closer), then finally the base **English**. The first locale in the chain that actually has content in the field is the one visitors will see.

> [!NOTE] Ask your developers to set up fallback locales
> While the *concept* of fallback locales is explained on this page, the actual *implementation* of it requires changes in your website query code. You'll need your developers' or agency's help with this. If this feature is useful to you, please discuss it with them and point them at this corresponding developer-facing documentation: [Localization](/docs/content-delivery-api/localization.md)

## Locale-based publishing

By enabling the optional locales settings, teams have the flexibility to publish content for specific locales within their project, regardless of the status of other locales.

For instance, imagine a project with locales for Germany, Switzerland, Great Britain, and Belgium. With this feature, teams can focus on creating and finalizing content for Germany without the need to manage content for other locales. If the team has the capacity to work on additional locales, they can save the content as drafts without publishing it. This enables multiple team members to independently create content for different locales, aligning with their respective timelines and priorities.

When a team member is prepared to publish content for a specific locale they have permission for, they can simply select the "Only publish specific content" option.

A convenient popup window will then appear, allowing them to choose which locale(s) to publish, with the ability to select multiple locales if desired:

(Video content)

You can also selectively unpublish one (or multiple) locales:

(Video content)

Locale-based publishing also works on scheduled publications/unpublishing:

(Video content)

## Translator roles, and locales permissions

Our roles/permissions system allows specifying which locales each collaborator can add/edit/remove on any record. For each role you can define both global rules, which will be applied to all models in your project, and specific per-model rules, giving maximum flexibility:

(Image content)

Every role can customize which locales can be edited

## Localized CMS interface

By default, the CMS interface will pick the default browser's language and, if available, will show the interface localized.

If you prefer to manually pick one, you can do it like this:

(Video content)

If you don't find the translation that you need, and you are looking into contributing, read [this blog post](https://www.datocms.com/blog/backend-community-translation.md) to learn more and get involved.

#### Learn more about localization with DatoCMS

DatoCMS allows a great deal of customization when dealing with localization. Check out these tutorial videos for a hands-on approach:

[

(Image content)

Localizing Content in DatoCMS

Play video »

](https://youtu.be/166gt1Qg-d4)

[

(Image content)

Creating a localized blog using Next.js

Play video »

](https://youtu.be/3tBeOwdVuwo)

---

# General concepts — Visual Editing

Source [docs]: https://www.datocms.com/docs/general-concepts/visual-editing.md

Visual Editing lets content editors click directly on any element of your website to edit it in DatoCMS, without hunting through forms and fields. Combined with draft content and real-time updates, editors see changes reflected instantly on the page as they type.

## Click-to-edit on the website

Editors can visit the website in draft mode and interact with content right there. Hovering over any editable element (a title, body text, an image alt text) reveals a subtle overlay. Clicking it opens DatoCMS directly at the exact field that controls that piece of content:

(Video content)

Click-to-edit overlays

## Side-by-side editing inside DatoCMS

The [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) brings a live preview of your website directly into the DatoCMS interface. A "Visual" tab provides a full-screen, side-by-side editing view where editors click on any element in the preview and the corresponding record and field open right next to it.

The connection works in both directions: browsing records in DatoCMS navigates the preview to the corresponding page, and clicking around in the preview opens the relevant record in the CMS.

(Video content)

Side-by-side editing

## Real-time feedback

When combined with [Real-time Updates](/docs/real-time-updates-api.md), editors see their changes reflected on the preview as they type. There's no need to reload: the preview updates live, giving editors immediate confidence that their changes look right in context.

## Getting started

Visual Editing is available on every DatoCMS plan (including Free) and works in any environment. The easiest way to get started is from one of our [tech starter kits](https://www.datocms.com/marketplace/starters.md), which come pre-configured with everything wired together. For a detailed walkthrough of how it works and how to set it up, see the [Visual Editing guide](/docs/visual-editing.md).

---

# General concepts — Record-level collaboration: Presence & locking

Source [docs]: https://www.datocms.com/docs/general-concepts/collaboration-features.md

Our collaboration tools help you manage teamwork and ensure that no data is lost when switching between users.

DatoCMS manages modifications to records, assets, and models in real-time, without the need for other editors to refresh the page.

What it means is that every change you make is immediately visible to every user, from record creation to asset deletion. You can also add, edit or reorder fields in a model while others are working on affected records without losing their work.

## Presence Indicator

A presence indicator is visible when another user opens or edit a record, with a notification that tells you if the user is either looking or editing the record.

(Video content)

## Locking and Unlocking a Record

To prevent two users from changing the same record at the same time, we have implemented an automatic lock.

When a user starts editing a record, it will be considered locked and therefore not editable by other users. The record will be available again as soon as the first user saves or closes the editor.

(Video content)

You can forcefully unlock a record if your role on DatoCMS has the authority to do so. The other user will be kicked out from the editing session, and the record will be locked by you.

(Video content)

To avoid the involuntary loss of content, you can recover the work done by the previous user and start from their unsaved changes.

To have an overview of all DatoCMS features, check this video tutorial:

[

(Image content)

A gentle overview of all the features of DatoCMS

Play video »

](https://www.youtube.com/watch?v=ALHwdztg0UQ)

---

# General concepts — Workflows

Source [docs]: https://www.datocms.com/docs/general-concepts/workflows.md

Larger teams often stumble through many bottlenecks caused by disconnected systems, duplicate content, and inefficient workflows. Organizations invest more in content, but their ROI remains lower due to friction, and their content engines stall.

With Workflows, you can **set up a precise state machine** that can bring a draft content **from initial creation to final publication** (and beyond), through a series of intermediate, fully customizable approval steps, keeping the whole team in sync without scattering the process across a number of external software tools to keep track of what needs to be done.

(Image content)

A simple example of what you can achieve with DatoCMS workflows

## How it works

A workflow is composed of a series of **stages**, which are essentially labels and a description. One of the stages has to be marked as the initial one, so that new records will start from there:

(Image content)

A workflow is composed of a series of stages, which are essentially labels

Within the same DatoCMS project, you can create multiple workflows and assign them to different models. When you apply a workflow to a model, all its existing records will be assigned the initial stage:

(Image content)

Workflows can be assigned to multiple models

Once everything is configured, users will be able to filter records by stage — optionally creating saved filters and sharing them with the team — and move records from one stage to another, in batch or one at a time:

(Video content)

The final experience for your editors. Clean, simple, secure.

Using our [roles and permissions system](/docs/general-concepts/roles-and-permission-system.md), you can **specify exactly which team members are in charge of performing the necessary checks and operations on the content** so that it can advance to the next step in the approval chain and the team never publishes something by mistake.

## How to configure workflows

**Workflows are completely custom**: you are free to tailor the stages you need with no limits, following your organization's natural processes. For this example, we'll create a new workflow with three stages:

-   **Writing**
-   **In review**
    
-   **Approved**
    

For this workflow, we also want to enforce the following simple rules:

-   **Creators** work on the content in the *Writing*stage. When they're done, they move articles to the *In review* stage, so that..
-   **Editors** can either reject or approve them, moving them back to *Writing* or forward to *Approved* stage;
    

The first step is actually creating the workflow itself. Go to **Configuration \> Workflows**, and create the following workflow:

(Image content)

The second step is to assign this workflow to one or more models. You can do so by entering the **Schema \> Models** area, selecting a model and clicking on "Edit model":

(Image content)

We can move back to Configuration**\> Content permissions** to specify which actions and transitions between stages are allowed for the two **Creator** and **Editor** roles. This what you need to setup for the Creator role, for example:

(Image content)

The permissions are pretty self-explanatory, and refer to the same set of rules we have determined at the beginning. Rules can be both positive or negative — to allow or block a specific permission.

You can setup rules for every model under a specific workflow, or even override some rules for some of your models. In this example, on top of all the rules enforced on all models that are under the workflow, we additionally block publishing only for blog posts:

(Image content)

## Workflows is an Enterprise feature

Workflows is a feature **available only to Enterprise customers.** Whichever plan you are on, you can still create and configure workflows to see if they solve your needs, but you won't be able to actually associate them with any model. If your company is interested in this feature, please [contact our Sales team](https://www.datocms.com/contact.md) for more details on pricing; we'll be happy to offer you a trial!

To have an overview on all DatoCMS features, check out this video tutorial:

[

(Image content)

A gentle overview of all the features of DatoCMS

Play video »

](https://www.youtube.com/watch?v=ALHwdztg0UQ)

---

# General concepts — Customize CMS domain

Source [docs]: https://www.datocms.com/docs/general-concepts/customize-cms-admin-domain.md

To help your editors find the CMS URL, or to provide a more white-labeled solution, you can customize the URL of your project's CMS. Instead of the default `yourproject.admin.datocms.com`, you can use your own domain, like `cms.mycompany.com`.

To do so, first make sure the `CNAME` record of your domain's DNS points to `admin.datocms.com`, then go to your dashboard and follow along:

(Video content)

> [!NOTE] The CMS domain only affects the DatoCMS admin area
> Please note that this domain only affects your CMS admin area, the place you normally go to edit your project and its models and records.
> 
> It does NOT affect the domain name for:
> 
> -   Your frontend (see [How your website and DatoCMS work together](/docs/general-concepts/how-your-website-and-datocms-work-together.md) )
>     
> -   Your DatoCMS account dashboard (dashboard.datocms.com)

---

# General concepts — Webhooks

Source [docs]: https://www.datocms.com/docs/general-concepts/webhooks.md

If you need to know when data has changed in one of your projects, you can create customized webhooks to get HTTP notifications as soon as the events occur.

For example, you might use webhooks as the basis to:

-   Integrate/sync DatoCMS data with third-party systems (Snipcart, Shopify, Algolia, etc.);
-   Get Slack/email notifications;
    
-   Automatically post an update on Facebook/Twitter;
-   Produce an automatic deploy on your staging environment;
    

You can connect DatoCMS webhooks to any endpoint you like — for example, some custom AWS lambda function.

> [!PROTIP] Pro tip: DatoCMS + Zapier: no-code management of webhooks!
> If you prefer not to write code, you can use [Zapier Webhooks](https://zapier.com/page/webhooks/) to connect a DatoCMS event with hundreds of different external services, creating any kind of complex automation workflow.

## Setting up a webhook

You can set up a new webhook under the *Project Settings \> Webhooks* section of your administrative area. You can enter any URL as the destination for calls, add HTTP basic authentication and custom HTTP headers:

(Image content)

DatoCMS needs to get a status code `2XX` reply from the configured URL to confirm that the notification sent via HTTP POST has been successfully delivered. If any webhook returns a different status code or times out, DatoCMS will set the status as "Failed".

### Webhook triggers

Webhook triggers let you specify under which circumstances an HTTP call will be performed towards your endpoint:

(Image content)

You can add as many triggers as you want to a single webhook. DatoCMS supports events for the following objects:

| Entity | Available events | Additional notes |
| --- | --- | --- |
| Record | `create`, `update`, `delete`, `publish`, `unpublish` | You can trigger the webhook only for specific records or records belonging to specific models. See the "Record Lifecycle Events" section for details. |
| Model | `create`, `update`, `delete`, | You can trigger the webhook only for specific models. Changes made to a model's field will trigger a call as well. |
| Upload | `create`, `update`, `delete` |  |
| Build trigger | `deploy_started`, `deploy_succeeded`, `deploy_failed` |  |
| Environment | `deploy_started`, `deploy_succeeded`, `deploy_failed` |  |
| Maintenance Mode | `change` | Triggers whenever an admin activates or deactivates the maintenance mode. |
| SSO User | `create` | Triggers when an SSO User is added to a project as a collaborator. |
| CDA Cache Tags | `invalidate` | Triggers when CDA Cache Tags need to be invalidated. |

Visit the [Data consistency: key concepts and implications](/docs/content-modelling/data-migration.md) section for more details on when the webhooks related to the records will be triggered.

## The HTTP Payload

DatoCMS will perform an HTTP POST request towards the specified endpoint. The HTTP body will be in JSON format, and will contain all the information relevant to the event just happened.

The body will contain the following information:

| Payload property | Description |
| --- | --- |
| `site_id` | ID of the project where the event occurred. |
| `webhook_id` | ID of the webhook that triggered the delivery. |
| `environment` | ID of the environment where the entity resides. |
| `is_environment_primary` | Whether the environment where the event occurred is the primary environment. |
| `webhook_call_id` | ID of the specific webhook event that triggered. |
| `event_triggered_at` | Date when the event originally occurred. |
| `attempted_auto_retries_count` | If auto-retry is on for the webhook, this field displays the number of the current attempt. |
| `entity_type` | The type of entity that triggered the webhook (ie. item, item\_type...) |
| `event_type` | The type of event that triggered the webhook (i.e.: create, update, delete...) |
| `entity` | The full payload of the entity serialized according to our Content Management API schema. |
| `previous_entity` | Only present if the event type is "Record \> Update". It represents the serialized record BEFORE the update (useful to know what changed). |
| `related_entities` | An array containing all serialized entities specified in the entity's relationships. |

As an example, in the case of a *Record \> Update* event, you can access the record state both before the update operation (`previous_entity`) and after (`entity`), making it easier to make a diff and see exactly what fields in the record changed:

```json
{
  "site_id": "example-site-id",
  "webhook_id": "123",
  "environment": "foo-bar",
  "is_environment_primary": true,
  "webhook_call_id": "456",
  "event_triggered_at": "2024-08-26T14:30:00Z",
  "attempted_auto_retries_count": 3,
  "entity_type": "item",
  "event_type": "update",
  "entity": {
    "id": "39830648",
    "type": "item",
    "attributes": {
      "name": "Mark Smith"
    },
    "relationships": {
      "item_type": {
        "data": {
          "id": "810928",
          "type": "item_type"
        }
      },
      "creator": {
        "data": {
          "id": "42011",
          "type": "account"
        }
      }
    },
    "meta": {
      "created_at": "2018-10-28T18:44:32.776+01:00",
      "updated_at": "2021-08-17T09:11:56.145+02:00",
      "published_at": "2021-08-17T09:11:56.143+02:00",
      "first_published_at": "2018-10-28T18:44:32.789+01:00",
      "status": "published",
      "current_version": "117626080"
    }
  },
  "previous_entity": {
    "id": "39830648",
    "type": "item",
    "attributes": {
      "name": "John Smith"
    },
    "relationships": {
      "item_type": {
        "data": {
          "id": "810928",
          "type": "item_type"
        }
      },
      "creator": {
        "data": {
          "id": "42011",
          "type": "account"
        }
      }
    },
    "meta": {
      "created_at": "2018-10-28T18:44:32.776+01:00",
      "updated_at": "2021-08-17T09:11:53.371+02:00",
      "published_at": "2021-08-17T09:11:53.367+02:00",
      "first_published_at": "2018-10-28T18:44:32.789+01:00",
      "status": "published",
      "current_version": "117626079"
    }
  },
  "related_entities": [
    {
      "id":"810928",
      "type": "item_type",
      "attributes": {
        "name": "Author",
        "api_key": "author",
        ...
      },
      "relationships": { ... }
    }
  ]
}
```

### Customize the URL or HTTP payload

If you want, you can also customize the HTTP body of the outgoing requests. To do that, hit the *Send a custom payload?* switch and provide the new payload.

You can use the [Mustache language](https://mustache.github.io/) to make the payload dynamic. The original payload we would send is used as source for the template. You can experiment with the Mustache language in their [sandbox](https://mustache.github.io/#demo), or read their [docs](https://mustache.github.io/mustache.5.html).

As an example, this custom payload template:

```json
{
  "message": "{{event_type}} event triggered on {{entity_type}}!",
  "entity_id": "{{#entity}}{{id}}{{/entity}}"
}
```

Will be converted into the following HTTP body:

```json
{
  "message": "update event triggered on item!",
  "entity_id": "123213"
}
```

You are not limited to send JSON payloads: just make sure that if the payload is not in JSON format, you configure the proper `Content-Type` header.

Similarly, you can also insert Mustache tags in the webhook URL.

## Automatic Retries

Optionally, you can activate the **Automatic Retry** option in your webhook settings, so that in case of delivery failure, DatoCMS will attempt to resend the request up to 7 times, with increasing intervals between each attempt.

(Video content)

Each retry will use the most recent webhook settings, and the retry schedule is as follows:

| Retry | Time |
| --- | --- |
| 1 | 2 minutes after the failure |
| 2 | 6 minutes after the previous retry |
| 3 | 30 minutes after the previous retry |
| 4 | 1 hour after the previous retry |
| 5 | 5 hours after the previous retry |
| 6 | 1 day after the previous retry |
| 7 | 2 days after the previous retry |

## Understanding webhook statuses

Webhook calls can have different statuses to indicate the outcome of the delivery attempt:

| Status | Description |
| --- | --- |
| Pending | The webhook call is currently being executed. |
| Success | The webhook call was successfully delivered to the specified endpoint, and the server responded with an HTTP status code in the 2xx range. |
| Failed | The webhook call could not be successfully delivered. This may be due to issues such as server errors, invalid endpoints, network problems or an HTTP status code not in the 2xx range. |
| Rescheduled | The webhook delivery failed, but is scheduled to be retried automatically based on the webhook automatic retries setting. |

## Debug and keep track of webhooks activity

You can browse webhook activity under the Project Settings \> *Webhooks activity log* section of your project, or [using our API](/docs/content-management-api.md#webhook_call-0). In both cases, you can filter/order webhook calls to refine your search based on various criteria, such as status, type of event, date, etc:

(Image content)

## Manually Resend Webhook Event

At any time you have the option to resend a webhook manually. To do so, click on the "Details" link and then on "Resend now"

(Image content)

When you choose to manually resend a webhook call, the system will repeat the exact same call with the updated webhook settings. If auto-retries are enabled:

-   a successful manual resend will stop further auto-retry attempts,
-   a failed manual resend won't add to the count of automatic retries.
    

## Webhook Timeouts

DatoCMS enforces two timeout limits for webhook integrations:

-   **Connection Timeout: 2 seconds**  
    This is the maximum time allowed to establish the initial connection to the webhook's HTTP server.
    
-   **Total Execution Timeout: 8 seconds**  
    This is the maximum time allowed for the entire webhook process to complete.
    

If your service exceeds either of these timeouts, DatoCMS will terminate the connection. The delivery attempt will then be marked as either Failed — or Rescheduled, if Automatic Retries are enabled.

> [!PROTIP] Pro tip: Prefer asynchronous over synchronous
> Due to the unpredictable nature of service completion times, it's recommended to handle the bulk of your processing in background jobs. This approach helps manage DatoCMS's timeout constraints effectively. Consider using job queue libraries such as Resque (Ruby), RQ (Python), or RabbitMQ (Java).
> 
> The pattern we suggest is to perform the initial validation checks of the payload quickly and synchronously before starting the background jobs. This allows you to potentially respond with a status code other than `2XX` to the webhook, thereby notifying DatoCMS of the issue.

### Webhook events for record lifecycle changes

This section clarifies how webhooks are fired on record lifecycle changes (such as publication or deletion). The behavior will be different if the model has the [draft/publish system](/docs/general-concepts/draft-published.md) enabled. See the following tables for details.

##### With draft/publish system enabled

| When a record is... | These events will be sent | `entity.meta.status` |
| --- | --- | --- |
| Saved for the first time | `create` | `draft` |
| Modified & saved again without publishing | `update` | `draft` |
| Published | `publish` | `published` |
| Modified & saved after publishing | `update` | `updated` |
| Selectively published (e.g., one locale gets selectively published, but there is still saved-but-unpublished data in other locales) | `publish` | `updated` |
| Scheduled to publish / unpublish | `update` | `updated` |
| Unpublished | `unpublish` | `draft` |
| Deleted from `draft` status | `delete` | `draft` (even though record is gone) |
| Deleted from `published` status | `unpublish` `delete` | `published` (even though record is gone) |

##### With draft/pub system disabled

When draft/publish system is **disabled** on a model, the `publish` and `unpublish` events will still be sent as they're implicit with record creation, update and deletion.

This will result in multiple events sent for each user action on a record, with the benefit of having a uniform way to listen for record changes via webhooks, regardless of the model draft/pub preference:

| When a record is... | These events will be sent | `entity.meta.status` |
| --- | --- | --- |
| Saved for the first time | `create` `publish` | `published` |
| Updated | `update` `publish` | `published` |
| Deleted | `unpublish` `delete` | `published` (even though the record is gone) |

---

# General concepts — Plugins

Source [docs]: https://www.datocms.com/docs/general-concepts/plugins.md

While DatoCMS already offers a very wide range of options and configurations, with plugins it is possible to take a leap forward and integrate market-leading third-party services with the DatoCMS platform, or build custom integrations tailored specifically to your business.

### What can plugins do?

A better question is — what do you want to achieve with plugins? Using plugins, a huge variety of enhancements to the DatoCMS web app are possible, from small field editor improvements to deeply integrated full-page applications. The [Plugin SDK](/docs/plugin-sdk/introduction.md) makes customizing the web app effortless.

Some common use cases are:

-   Adding custom field editors to improve the editor experience;
-   Managing content versions for running A/B tests on structured content using personalization tools;
    
-   Customizing the default entry editor to suit your specific needs;
-   Seamlessly integrating DatoCMS with third-party software and services;
    

### Managing and distributing Plugins

#### Private plugins

A private plugin is built by you for your specific organization's needs to optimize your organization's editorial experience. It is fully under your control and not accessible by other organizations. The total number of plugins and installations within your organization/environment is limited based on your DatoCMS plan.

#### Marketplace plugins

[Marketplace plugins](https://www.datocms.com/marketplace/plugins.md) are built by our community and connect DatoCMS with other systems allowing you to assemble the stack of your choice. Everyone can (and is encouraged to) contribute with new plugins by releasing them as NPM packages.

More than 100 plugins are already available on the Marketplace, and can be installed free of charge without touching a single line of code. Installation is extremely simple, and can happen both programmatically or using the interface.

### Installing Marketplace Plugins

To install a new plugin for your project, go to **Configuration \> Plugins** and click on **Add a new plugin**.

This action will open the **Plugin Marketplace** directly within your DatoCMS backend, allowing you to browse all available community plugins. You can filter by categories such as *Most Popular*, *Recently Released*, or use the search bar to find plugins by keyword.

When you find a plugin you’d like to use, click on its card to open the details page. Here, you’ll see a description, metadata, and other relevant information about the plugin. Simply click the **Install** button to add it to your project.

Once installed, the modal will close, and the newly added plugin will appear in the **Plugins** list, ready for configuration.

(Video content)

If you're not sure what plugins you need and want some inspiration, we've also curated commonly used plugins under collections like **Editor Favorites** and **Dev Favorites** to make it simpler for you to pick and choose the greatest hits.

### Creating new Plugins

To learn how to build new plugins, and maybe share them with the community, please visit our [detailed guide](/docs/plugin-sdk/introduction.md) or take a look at this video tutorial on how to start developing a plugin from scratch.

[

(Image content)

Intro to the Plugin Ecosystem

Play video »

](https://www.datocms.com/user-guides/the-basics/intro-to-the-plugin-ecosystem.md)

[

(Image content)

How to start developing plugins for DatoCMS

Play video »

](https://youtu.be/sc8sm34tyWw)

---

# General concepts — DatoCMS Site Search

Source [docs]: https://www.datocms.com/docs/general-concepts/site-search.md

DatoCMS Site Search is a way to **deliver tailored search results to your website visitors**. You can think of it as a replacement for the now discontinued Google Site Search.

(Image content)

There are many third-party services out there that fill this need (like [SwiftType](https://swiftype.com/), [Algolia](https://www.algolia.com/), and [Cludo](https://www.cludo.com/)). Our solution seeks to be a great option for plenty of websites:

-   Extremely easy to integrate with your static website
-   Completely customizable in terms of look & feel
    
-   Minimal configuration needed
-   Handles multilingual websites nicely
    
-   included in the price of DatoCMS with no additional charges
    

#### How it works

-   Every time your website finishes being deployed, **we'll crawl it to fetch updated content.**
-   From your frontend, you can [**make AJAX requests to our Content Management API**](/docs/site-search/base-integration.md#performing-searches) **to present relevant results to your visitors**. We also provide [**React**](/docs/site-search/widget.md) **and** [**Vue**](/docs/site-search/vue-search-widget.md) **search widgets** that simplify the process.
    

> [!PROTIP] Pro tip: Integrating Algolia and DatoCMS
> If you prefer to integrate a search provider like Algolia, [this guide](https://www.datocms.com/blog/algolia-nextjs-how-to-add-algolia-instantsearch.md) demonstrates setting up a Next.js project, configuring Algolia, and creating custom search components. While the guide focuses on Algolia Intellisearch, the process for setting up other third-party services like Meilisearch, Typesense, or ElasticSearch should be relatively similar.

#### Enabling Site Search for a project

To get started, please see [Configuring DatoCMS Site Search](/docs/site-search/configuration.md).

---

# General concepts — Project Templates

Source [docs]: https://www.datocms.com/docs/general-concepts/project-starters-and-templates.md

DatoCMS allows you to turn an existing project into a ready-to-clone public template project allowing anyone to bootstrap a new project based off of yours.

In this guide you will learn how to make a project public and how to create and configure a clone and deploy link or button to share your project.

## Turn a project into a public template

Since projects might contain sensitive information they are all private by default. To make a project public, head to the project main page in the DatoCMS dashboard and switch on the **Public template** project option in the *Danger Zone* section.

**Important**: From now on anyone will be able to clone the project, so make sure it doesn't contain any sensitive information!

(Image content)

Once you've set your project to be a public template, you can then generate:

-   A "Clone project" button to perform a complete clone of an existing DatoCMS project, or
-   A "Project starter" button, to clone a project AND deploy a frontend capable of reading the content coming from the project itself.
    

## Generate a "Clone project" button

The "Clone project" button helps users perform a complete clone of an existing DatoCMS project. Once clicked, they will see the following dialog, and at the end of the process a copy of the original project will be available on their dashboard:

(Image content)

The "Clone project" dialog

Use the form below to generate a ready-to-use clone button (the project ID can be retrieved [inside the details page of the project](/docs/general-concepts/project-starters-and-templates.md#project-id)):

Project ID \* 

Project Name \* 

Use the following code to share the button on your README file or documentation:

URL

Markdown

HTML

Button Preview

(Image content)

## Generate a "Project Starter" button

Most of the time, a DatoCMS project is associated with a frontend project (website, application, etc.) that knows how to query for its content, and renders the result in a pleasant way to users. The "Project starter" button helps users deploy new sites from templates with one single click, performing the following actions for them:

1.  Clone a DatoCMS template project and put the copy inside the user account;
    
2.  Fork a Git repository containing the frontend project inside the Github account of the user;
    
3.  Build and publish the frontend online using a free hosting solution (Netlify, Vercel, etc.)
    

Check out our [Marketplace](https://www.datocms.com/marketplace/starters.md) to see a fine selection of Project Starters.

(Image content)

The dialog that your users will see once they click on a Project Starter button

Project Starters are composed of a [DatoCMS template project](/docs/general-concepts/project-starters-and-templates.md#turn-a-project-into-a-public-template), plus a Git repository containing a `datocms.json` configuration file that specifies both presentational metadata (name, preview image, URL of an example of a successful deployment) and the information necessary for creating a new project.

You can use the form below to generate a `datocms.json` configuration file and a button to share the starter with the world:

Project starter name

Description

Frontend preview screenshot

URL of an example of a successful deployment

Github repository that will be copied

DatoCMS Project ID that will be duplicated

How the project can be built and deployed?Please select one...Simply make a copy of the template repositoryIt can be deployed to any static hosting (Vercel, Netlify)It can be deployed only to VercelIt can be deployed only to NetlifyIt can be deployed only to Heroku

##### Result

Copy the following code and add it to your Git repository in a file called `datocms.json`:

{
  "name": "THIS FIELD IS MANDATORY. PLEASE PROVIDE A VALUE!",
  "description": "THIS FIELD IS MANDATORY. PLEASE PROVIDE A VALUE!",
  "previewImage": "THIS FIELD IS MANDATORY. PLEASE PROVIDE A VALUE!",
  "datocmsProjectId": "THIS FIELD IS MANDATORY. PLEASE PROVIDE A VALUE!",
  "deploymentType": "copyRepo",
  "environmentVariables": {}
}

Use the following code to share the button on your README file or documentation:

MarkdownHTMLURLButton Preview

\[!\[Clone DatoCMS project\](https://dashboard.datocms.com/clone/button.svg)\](https://dashboard.datocms.com/deploy?repo=YOUR-GITHUB-REPO)

##### Project ID

The project ID can be retrieved inside the details page of the project in your Dashboard:

(Image content)

You can find your project ID in your dashboard

##### Supported deployment methods

The `deploymentType` setting allows you to configure what deployment target can be used during the cloning process. By setting this value to `copyRepo`, DatoCMS will clone the template on a repository in the user org or account.

Additionally, DatoCMS supports the following deployment types:

-   `vercel`
-   `netlify`
    
-   `static` (user can choose between Vercel and Netlify)
    

When one of these is chosen, users will be asked to authenticate on the service and therefore they need an active and valid account. Once authorized, the DatoCMS integration will deploy the template repository to the service.

##### Build command

When the deployment type is either `static`, `vercel` or `netlify`, you must specify the build command that will be run during the deployment of the frontend repository. DatoCMS will forward the `buildCommand` to the deployment service which will use it to build the application.

##### Environment variables

When the deployment type is either `static`, `vercel` or `netlify`, you can specify a number of environment variables that will be configured on the hosting platform, before building the actual frontend. The value of each environment variable can be either:

-   A custom string
-   The URL of the cloned DatoCMS project (ie. `https://<YOUR_PROJECT>.admin.datocms.com/`)
    
-   One of the DatoCMS API tokens present in the template project (you need to specify the name of the API token, ie. "Read-only API token")
    

##### Post-deploy install URL

When the deployment type is either `static`, `vercel`, or `netlify`, you can call a custom hook present in the frontend to add more complex configuration steps.

The hook must support CORS for the https://dashboard.datocms.com `Origin`, and will receive a POST request.

If the frontend is deployed to Netlify, the HTTP request body will be the following:

```json
{
  "datocmsApiToken": <DATOCMS_READWRITE_API_TOKEN>,
  "integrationInfo": {
    "adapter": "netlify",
    "netlifySiteId": <NETLIFY_API_TOKEN>,
    "netlifyToken": <NETLIFY_API_TOKEN>,
  },
}
```

If the frontend is deployed to Vercel, the HTTP request body will be the following:

```json
{
  "datocmsApiToken": <DATOCMS_READWRITE_API_TOKEN>,
  "integrationInfo": {
    "adapter": "vercel",
    "vercelApiToken": <VERCEL_API_TOKEN>,
    "vercelTeamId": <VERCEL_TEAM_ID>,
    "vercelProjectId": <VERCEL_PROJECT_ID>,
  },
}
```

---

# General concepts — How your website and DatoCMS work together

Source [docs]: https://www.datocms.com/docs/general-concepts/how-your-website-and-datocms-work-together.md

This article is for DatoCMS users who are not quite sure how a CMS connects to a website, or how a "headless" CMS affects editing workflows. We will explain:

-   [**How the pieces of a CMS-driven website fit together**](/docs/general-concepts/how-your-website-and-datocms-work-together.md#how-the-pieces-of-a-cms-driven-website-fit-together)
-   [**What a CMS & headless CMS are**](/docs/general-concepts/how-your-website-and-datocms-work-together.md#what-a-cms-and-headless-cms-are)
    
-   [**What you can and cannot edit in DatoCMS**](/docs/general-concepts/how-your-website-and-datocms-work-together.md#what-a-cms-and-headless-cms-are)
-   [**Who to contact for help**](/docs/general-concepts/how-your-website-and-datocms-work-together.md#who-to-contact-for-help)
    

## **How the pieces of a CMS-driven website fit together**

-   **The two main parts:**
    
    -   **CMS (Content Management System):** This is where you and your team create, organize, and update your content — your articles, images, videos, and more. DatoCMS is one such content management system, and specifically, a *headless* one. More on that later.
        
    -   **Frontend:** This is what visitors actually see when they go to `your-website.com`. They never directly use DatoCMS the way you do. Your frontend asks our service for the content, but then it takes that content and transforms it into the actual web pages, text, and images that your visitors see.
        
        When you hear the word "website" in casual conversation, the frontend is usually what is actually meant. In technical terms, your frontend is where all the HTML, Javascript, and CSS live, and together they form the webpage that browsers like Chrome and Safari show to your visitors.
        
        When you need to change some part of your website that isn't strictly "content" — its fonts, colors, behavior, etc. — it is likely the frontend that you (or your developers or web agency) would need to change.
        
-   **Other optional parts:**
    
    -   **Backend:** Some websites also have a behind-the-scenes system for handling ecommerce purchases, complex forms, or other advanced data processing that's better suited for the cloud. Not every site needs one of these, and only rarely does a backend connect to the CMS. Usually, a frontend + CMS is enough. Your visitors never directly see your backend; just like with the CMS, your frontend stands between your visitor and the rest of your website, and the frontend is all they ever see.
        
    -   **Web host(s):** One or more servers that your frontend and backend code live on. This could be a company like Vercel or Netlify, cloud services like Amazon Web Services or Azure, or rented virtual machines from smaller providers. Importantly, DatoCMS is NOT a webhost, and we do not hold any of your frontend or backend code.
        
    -   **Content Delivery Networks (CDNs):** These are global networks that hold copies of your frontend (or parts of it) for global distribution, making your website faster for users all over the world. A CDN lets visitors access your website from data centers close to them, instead of having to connect to a web host half the world away (which would be much slower).
        
    -   **DNS servers, domain registrars, and more:** Behind the scenes, many more services work together to ultimately enable visitors to find your website at `your-website.com`. A full explanation of all of them is beyond the scope of this article, but thankfully, these are often set-and-forget and don't require day-to-day maintenance. If anything does go wrong with them, your developers, web agency, and/or web host should be able to help. DatoCMS also does not include any of these services.
        

## What a CMS and *headless* CMS are

A CMS is a content management system, a place to store, edit, and retrieve your content easily for use and re-use across one or more websites/apps/frontends.

**Traditional CMSes manage your content and website frontend and backend together** in one big app. This heavyweight, monolithic approach was common for traditional web teams who wanted everything together in one place, inside one app, with full control of all of it.

**Headless CMSes are a lightweight alternative that only deal with the content, not the rest of the website**. This was an evolutionary development in CMS design that enabled more flexibility and modularity. A "headless" setup allows CMS providers (like DatoCMS) to focus on just the CMS, a frontend company to focus on the frontend, etc. Some teams prefer this modular, mix-and-match approach because they can customize each component and choose the best fitting ones for their team. It is also a deliberate separation of concerns that allows content editors to work on content and coders to work on code, neither stepping on the other's toes. New posts can be published without needing code changes, and code changes don't need to cause a content freeze. Each system and role can focus on their sphere of responsibility.

And why the term "headless"? Well, the frontend is one of the things that a headless CMS does NOT include. And the frontend is typically thought of as the "face" of a website. No face = no head.

Our service only give you the content "body" of a website, and it's up to your team to design one or more "faces" for it: the layout, the behavior, the fonts, the colors, etc. Maybe there's one website for computers and a separate app or phones, or maybe there's just one unified, responsive website for a variety of devices. In either case, that's the only part your team has to worry about.

In this headless setup, we provide the "body" and your team provides the "head":

-   Everything on **datocms.com** and **datocms-assets.com** is our responsibility and managed by us. (You still retain ownership and copyright of your content, of course.)
-   Everything on **your-website.com** belongs to your team, and it's up to your devs or web agency to manage those.
    

## What you can and *cannot* edit in DatoCMS

You can use DatoCMS to edit anything on **datocms.com** and **datocms-assets.com**:

-   Your content and schema, like your blog posts or the fields in them. You do this through the DatoCMS admin interface, like `your-project.admin.datocms.com`.
-   Your images and videos that you've uploaded to the media area. These are managed and edited through the same admin area as the rest of your project.
    

DatoCMS (the CMS software, and us, the staff) **cannot**:

-   Edit your website layout, fonts, colors, etc. Your frontend and backend live on a separate web host, not our servers.
-   Change your web hosting (like going from Vercel to Netlify, or fix issues with Amazon Web Services)
    
-   Change your domain name (like going from `your-website.com` to `new-website-name.com`)
-   Help with email inbox problems (like not getting email at `you@your-website.com` or deliverability issues)
    
-   Basically, anything not directly inside DatoCMS
-   **But if you're not sure... go ahead and ask us and we'll do our best to help you figure it out!**
    

## Who to contact for help

-   **Generally speaking, start with your own team of developers** (or the **agency that built your website**, if you don't have in-house developers). They should be able to diagnose and fix the issue directly, or at least know who can.
-   **DatoCMS support can only help you with DatoCMS issues**, which is any service on **datocms.com** or **datocms-assets.com**.
    
    We generally cannotdirectly help you with your website, e.g., anything on `your-website.com`. Your own team is the best resource for that.
    
    If you ask us anyway, we may take a look and try to provide some general guidance, but ultimately our reply is going to be some form of "this is what the problem looks like to us... you can share our findings with your developers, but they'll have to be the ones to fix it".
    
    We don't have any way to access, edit, or otherwise fix your website directly. We don't have your logins or access to your code. That's up to your team. But we'd be happy to take a look anyway, and offer whatever guidance we can.
    
-   If you're ever **not sure who to contact, that's perfectly OK, just reach out to us anyway!** 🙂 We don't expect you to be an expert at all this, and if you ever ask us about something we can't fix, we'll still try our best to point you in the right direction. You'll probably end up talking to your developers or web agency in the end, but at least you can start with us. We'll help you figure it out together.
    

You can contact us using [our support form](https://www.datocms.com/support.md#form?topics=technical-support%2Fgeneral-request) or directly via email at support@datocms.com.

---

# General concepts — How to deploy

Source [docs]: https://www.datocms.com/docs/general-concepts/deployment.md

Once you are all set with DatoCMS and your site is successfully pulling content on your local development machine, your next step is to deploy the site and then give your editors some control and visibility over the deploy process.

The job of building and deploying your website is not performed directly by DatoCMS, but is delegated to an external Continuous Deployment/Continuous Integration service.

To integrate DatoCMS with these tools, you can use what we call **build triggers**.

Essentially, they are a set of webhooks that you can manually trigger to launch your build process on your preferred continuous integration or continuous deployment platform.

We offer out-of-the-box integrations with all the most popular solutions out there (most of them have a free plan available):

-   [Netlify](https://www.datocms.com/marketplace/hosting/netlify.md)
-   [Vercel](https://www.datocms.com/marketplace/hosting/vercel.md)
    
-   [Gitlab CI](https://www.datocms.com/marketplace/hosting/gitlab.md)
    

If you need to use another CI tool, we also offer a [custom webhook](https://www.datocms.com/marketplace/hosting/custom-webhook.md) that you can use to connect DatoCMS to your custom deployment solution.

Once everything is set up, in the top navigation bar of the DatoCMS interface, you will find a "**Publish changes"** button: your editors will be able to request a new publication of the website whenever they like.

If you have multiple build triggers, you'll be able to trigger builds independently and manage permissions and logs for each environment.

Have a look at this quick demo to see how things work:

(Video content)

Check the Marketplace for all the available [Hosting and CI building](https://www.datocms.com/marketplace/hosting.md) options.

---

# General concepts — Primary and sandbox environments

Source [docs]: https://www.datocms.com/docs/general-concepts/primary-and-sandbox-environments.md

Traditional CMSs often treat content as a one-off effort, which makes content management difficult to fit into existing development lifecycles.

Content environments make it easier for your development team to **manage and maintain the content structure once your content has been published**. Think of environments as code branches: they're great for testing, development and pre-production.

In short, environments ensure quick turnaround times and flexibility for developers — without interrupting the editorial workflow.

### What's an environment?

By default, every project has one environment, called the **primary environment**, which is meant to be used for the regular editorial workflow. Additionally, developers can create multiple **sandbox environments** to safely test and experiment with changes in the content.

(Image content)

Sandbox environments start out as **exact copies of one of the existing environments** (i.e., the primary one). The process of creating a new sandbox from an existing environment is called **forking**.

Each environment is identified by a name (e.g., `master`) and stores the following information:

-   Models
-   Records
    
-   Uploads
-   Plugins
    
-   The content navigation bar
-   Configuration (locales, timezone settings, appearance, SEO preferences)
    

When making changes to any of the aforementioned entities in any environment, including the primary environment, **the data in all other environments remains unaffected**.

### Creating a new sandbox environment

To manage all your project's environments, head over to the *Project Settings \> Environments* section. To create a new sandbox starting from an existing environment, click on the contextual menu \> **Fork**, and choose a name for the new environment.

(Video content)

DatoCMS will perform a deep copy of all the information contained inside the source and transfer it to the new sandbox.

Once there's at least one sandbox environment, developers will be able to **switch environments using the top bar panel**.

Editors will never see this panel due to a reduced set of permissions and will continue their editorial workflow in the primary environment as usual.

(Video content)

### Promotion of sandbox environments

At any time, you can **promote a sandbox environment to become the new primary environment**. The old primary environment will be demoted to a sandbox environment, and content editors will immediately see the interface refresh. From that moment, they will only be able to see and make changes to the new primary environment.

To be updated when a sandbox gets promoted, you can [set up a webhook](/docs/general-concepts/webhooks.md#webhook-triggers) listening to the "Environment Promote" event.

### Renaming environments

At any time, you can change the name of an existing environment. This change won't impact those working on the CMS:

(Video content)

To be updated when a sandbox gets renamed, you can [set up a webhook](/docs/general-concepts/webhooks.md#webhook-triggers) listening to the "Environment Update" event.

### Forcing use of sandbox environments

Changes to a primary environment can be potentially disruptive, so we give you the ability to **block any user from editing the primary schema or configuration.**

You can do this by going to Project settings \> Global properties and enabling "**Force the use of sandbox environments".** If enabled, no user can edit the primary environment and make changes to its schema and configuration, regardless of their role.

---

# General concepts — Project usages

Source [docs]: https://www.datocms.com/docs/general-concepts/project-account-usages.md

On DatoCMS, usage quotas are tracked per account or per project. Let's see what they are, the differences between them, and where you can monitor them.

### Per-account resources

**Each account has quotas that are shared among all projects**. In particular, the shared resources are:

-   Records
-   File storage
    
-   API calls
-   Bandwidth
    
-   Video encoding
-   Video streaming
    

These resources can be monitored from your dashboard, [in the plan details](https://dashboard.datocms.com/plan-billing), where you can monitor how your resources are used across different projects, so you can better understand which ones you should optimize, or which of your clients should be billed more for their usage.

### Per-site resources

In each project you can drill down into the traffic, API calls and video streamed:

(Image content)

And you can change the reports, using this dropdown:

(Image content)

This helps you better understand where the traffic is coming from and how to best optimize the use of resources in your project.

---

# General concepts — Audit Logs

Source [docs]: https://www.datocms.com/docs/general-concepts/audit-logs.md

The Audit Logs functionality is for monitoring audit events happening in an Enterprise project and ensure continued compliance, safeguarding against any inappropriate system access, and allowing you to audit suspicious behavior within your enterprise.

The idea is to give Enterprise organization owners the ability to query user actions in a project. With Audit Logs, you can:

-   Automatically feed DatoCMS access data into a SIEM or other auditing tool
-   Proactively monitor for potential security issues
    
-   Write custom apps to gain insight into how your organization uses DatoCMS
    

An audit log provides insight into audit events that are actually happening across a DatoCMS project, and is therefore read-only and immutable.

You can filter for specific actions or actors to see who made changes on specific resources in the app using a very powerful SQL-like language called PartiQL, or use the Query Builder GUI that lets you build a filter using a familiar UI.

(Image content)

Actors can include both logged-in users as well as access tokens.

You can either browse and filter audit log events via the interface or through [API calls](/docs/content-management-api/resources/audit-log-event/query.md), and the retention window is fully customizable. By default, Audit Logs have a Time-to-Live (TTL) of two months from the date of writing. However, it is possible to customize the TTL for individual projects. To make such customizations, please contact [our support team](https://www.datocms.com/support.md), and we will be happy to assist you.

If you're interested in trying out Audit Logs for your projects, [contact our Sales team](https://www.datocms.com/contact.md) to set up a free trial.

---

# General concepts — How to manage a live and a preview site

Source [docs]: https://www.datocms.com/docs/general-concepts/how-to-manage-a-live-and-a-preview-site.md

As soon as you go live with a site you need to have a way to save and preview content before going live.

To do that in DatoCMS you can enable the [draft/published system](/docs/general-concepts/draft-published.md) on a per-model basis.

Once you have created your new draft records you might want to preview them, but how? You can combine the draft/published system with the [deployment environments](/docs/general-concepts/deployment.md) to achieve that.

For example you can have your live site pulling content from the main GraphQL endpoint and instead your *staging* pulling your drafts from the [preview endpoint](/docs/content-delivery-api/api-endpoints.md).

If you are using our REST API instead you can fetch the draft content using the `version` attribute, [for example on the records](/docs/content-management-api/resources/item.md#instances).

Finally you can leverage the deployment environments if you want to let your editors be able to manually trigger builds for the different sites.

See also: [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md), an official plugin that makes it easy to preview your changes inside or next to DatoCMS.

---

# Content modelling — Introduction to Content Modeling

Source [docs]: https://www.datocms.com/docs/content-modelling.md

DatoCMS can be seen as an editor-friendly interface over a database, so the first step is to build the actual schema upon which users will generate the actual website content.

The way you define the kind of content you can edit inside each different administrative area passes through the concept of models, which are much like database tables.

Each administrative area can specify a number of different models, and they represent blueprints upon which users will store the website content. For example, a site can define different models for articles, products, categories, and so on.

You can create new models in the *Settings \> Models* section of your project:

(Image content)

Each model consists of a set of fields that you define. Fields can be one of the following:

-   **Single-line string**: Ideal for titles, headings, etc.
-   **Multiple-paragraph text**: For simple Markdown, HTML or plain text.
    
-   [**Modular content**](/docs/content-modelling/modular-content.md): To define dynamic layouts for ie. landing-pages and give the content writers the choice between different template options.
-   [**Structured text**](/docs/content-modelling/structured-text.md): To store rich-text content, complete with images/videos/custom blocks using a portable JSON format.
    
-   **Asset gallery**: To store one or more files (for sliders, carousels, etc.).
-   **Single asset**: To store any kind of document (images, PDFs, ZIPs, videos, etc.).
    
-   **Video**: To reference to an external YouTube/Vimeo video.
-   **Date** and **DateTime**: A timestamp value for storing dates and times (i.e. an event start, office opening hours).
    
-   **Integer** and **Floating-point number**: For storing integer SKUs, quantities, prices, etc.
-   **Boolean**: For storing values that have two states, e.g., yes or no, true or false etc.
    
-   **Geolocation**: Coordinate values for storing the latitude and longitude of a physical location.
-   **Color**: For storing colors (with or without alpha channel).
    
-   **SEO meta tags**: To manage a page meta title, meta description, OpenGraph cards, etc.
-   [**Slug**](/docs/content-modelling/slug-permalinks.md): To generate a page permalink based on another textual field of the model.
    
-   [**Single and multiple links**](/docs/content-modelling/links.md): To model relationships between content, including other models. For example, linking a blog to a category.
-   **JSON**: For storing JSON objects.
    

(Image content)

Field type selection modal

Each field has a name and additional metadata, like validations, or particular configurations to better present the field to the editor (hints, etc.):

(Image content)

Validations tab in Field settings

(Image content)

Presentation tab in Field settings

Fields in DatoCMS can also be [localized](/docs/general-concepts/localization.md), if you need to accept different values based on language.

DatoCMS stores the individual pieces of content you create from a model as records, which are much like table rows in a database. You (and your editors) can create new records of a certain model within the *Content* tab of your administrative area:

(Image content)

#### New to DatoCMS?

If you want to get started with DatoCMS and learn the basics, check out these video tutorials for beginners!

[

(Image content)

Intro to the Schema Builder

Play video »

](https://www.datocms.com/user-guides/the-basics/intro-to-the-schema-builder.md)

[

(Image content)

Intro to Models in DatoCMS

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-models-in-datocms.md)

[

(Image content)

Intro to Fields in DatoCMS

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-fields-in-datocms.md)

---

# Content modelling — Single instance models

Source [docs]: https://www.datocms.com/docs/content-modelling/single-instance.md

Real-world websites have often pages which don't resemble any other (eg. the *About us* page, or even the homepage).

If you want to allow the editors to change their content, you can create a Single-instance model:

(Image content)

While *collection* models enable the creation of multiple records, *single-instance* models allow just a single item to be edited in the administrative area.

---

# Content modelling — Record ordering

Source [docs]: https://www.datocms.com/docs/content-modelling/record-ordering.md

The record collections can be ordered in different ways:

-   By the records that were last updated first (default ordering)
-   By one specified field, in ascending or descending order
    
-   In a tree-like structure
-   By drag and drop reordering
    

The default ordering should be quite self-explanatory.

The same goes for ordering by specified field. You can select the field and the ordering direction in the model settings:

(Image content)

The tree-like structure has [its own documentation page](/docs/content-modelling/hierarchical-sorting.md) where you can see it in action.

Last but not least, we have drag and drop reordering. In this case, once you select the appropriate choice from the usual dropdown, you will have the option of dragging and dropping the records in the collection list.

In case you need to move a record across pages, you can enter the record and change the position attribute in the right sidebar:

(Image content)

### Caveat

One thing to note about the drag and drop reordering and the tree-like structure reordering is that as soon as you change the position of a record, it's updated in the API, even for published records. This means you cannot have separate draft/published states for the position attribute.

---

# Content modelling — Hierarchical sorting (Tree-like collections)

Source [docs]: https://www.datocms.com/docs/content-modelling/hierarchical-sorting.md

> [!NOTE] Tree-like Collections are renamed to Hierarchical Sorting
> In 2025, we changed the name of this feature for better clarity. The underlying functionality is still the same.

Taxonomies, product categories, navigation bars... websites are full of hierarchical data. DatoCMS is the only headless CMS that supports tree-like data structures out-of-the-box, offering a delightful editing experience for your editors and marketers.

If you want to arrange a model collection as a hierarchy or tree, you need to select "Hierarchical sorting" in the model's Presentation settings, in the "Default collection ordering" field.

(Image content)

Hierarchical sorting, much like other models, is also presentable in a compact or a tabular view, depending on which appearance best suits your workflows.

(Video content)

Additionally, both the tabular and the compact view are paginated, and records are incrementally shown as they are loaded.

---

# Content modelling — Record block limits and byte size limits

Source [docs]: https://www.datocms.com/docs/content-modelling/record-block-limits-and-byte-size-limits.md

Records are subject to technical limits that apply regardless of your DatoCMS pricing plan. These limits are in place to protect the reliability and performance of our shared infrastructure.

## Record Block Limits

### Maximum number of blocks per record

Each record can have **up to 500 blocks, shared between all of a record's locales.** Examples:

-   A single locale can use all 500 blocks for itself
-   Two locales can split it 250 each, or 400 for one locale and 100 for the other
    
-   Five locales can split it 100 each, or any way they want, as long as the record total is less than 500
    

You do not need to "pre-assign" a particular number of blocks per locale. The limit is dynamically calculated as you type, and if you use more blocks in one locale, the others will naturally have fewer remaining. There is only ever one **per-record total blocks limit, not a per-locale one**.

### **How to see the number of blocks currently used**

**The record sidebar always shows the Currently used blocks**, e.g. `90/500 (18%)`.

Additionally, the system will show you a warning at the top of the record if you are at risk of hitting the record limit.

(Image content)

### Maximum block nesting depth

You can nest up to **5 layers of blocks** inside each other**.** If you reach the limit, you will see a "Reached maximum blocks depth!" warning:

(Image content)

### How to avoid hitting the blocks limit

We recommend keeping these limits in mind when you design your schema. Consider splitting up overly complex/long models into several smaller ones, and use a Link field to reference them from one another. Not only does this help you avoid reaching the limits, it also makes the editor experience more pleasant — it's generally easier to work on a few well-organized, mid-sized records than a single huge, long one.

## Record Byte Size Limits

Records must stay under **300 KB**, including the content in all of its **blocks, fields and locales.**

Please note that hidden content, such as invisible markup & metadata (most commonly found in HTML fields), also count towards this limit. You will see a warning below the field if we detect a field with excessive hidden content.

(Image content)

In that case, you should edit the source code to make sure there is no unnecessary metadata or other hidden content in the field. **The most frequent source of unexpected hidden content is copying & pasting from other desktop or web apps, such as word processors or design tools**, which can add metadata to their copied text for their internal use. Such metadata is not useful in the context of DatoCMS and your frontends and can usually be safely deleted.

## What to do if you hit the record block or size limits

First, please consider whether your model schema and fields can be modified to better fit within the limits. You can split up overly an complex model with too many fields into several smaller ones, for example, and use Link fields to reference them from one another.

For the cases where an extremely long page (privacy policy, legal terms & conditions, etc.) cannot be further subdivided and must have very long text and blocks in multiple locales, you can consider making one record per locale. This makes for a slightly awkward editor and developer experience, but it is better than running out of space or blocks altogether.

As a last resort, in exceptional cases, we *may* raise these limits after a technical evaluation of your project. Please [contact support](https://www.datocms.com/support.md?topics=technical-support%2Fgeneral-request) and we'll review your model schema and record contents. If we cannot find a suitable workaround, we may *slightly* increase the limits — but only if we determine, in our judgment, that doing so would not degrade performance or reliability for you or other customers.

---

# Content modelling — Blocks

Source [docs]: https://www.datocms.com/docs/content-modelling/blocks.md

Blocks are a concept unique to DatoCMS and are the foundation behind powerful flagship features such as [Modular Content](/docs/content-modelling/modular-content.md) and [Structured Text](/docs/content-modelling/structured-text.md), which we advise you to read about in detail.

In a sentence, though, blocks allow you to define **complex and repeatable structures that can be embedded inside records**. Modern web design often involves the use of repeated "graphic components" across pages — call-to-actions, sliders, testimonial quotes, etc. Blocks allow developers to clearly represent each of these objects, so that they can then be used and reused in the content of individual pages by marketers and content creators, giving them significant expressive freedom.

You can manage your Blocks Library inside the settings area of your project:

(Image content)

The "Blocks Library" section

## What can you do with Blocks?

You can use blocks in two different contexts, to achieve different results:

-   Using [Structured Text](/docs/content-modelling/structured-text.md) fields, you can produce great pieces of content by interleaving free-form text with blocks representing predefined graphic components (CTA, quotes, image galleries, infographics, etc).
-   Using [Modular Content](/docs/content-modelling/modular-content.md) fields, you can create a page-builder experience that enables your editors to assemble various blocks like Lego pieces, allowing for the construction of any dynamic layout — particularly beneficial for landing pages.
    

## Key concepts

-   Just like records, a block is a composition of fields, on which you can define custom validations;
-   Blocks defined in the library can be reused across different models;
    
-   Unlike records, **blocks do not exist independently, but only within a parent record.** For this reason, **blocks do not count towards your plan's records limit,** and cannot be referenced in [Link fields](/docs/content-modelling/links.md). They only live inside [Modular Content](/docs/content-modelling/modular-content.md) and [Structured Text](/docs/content-modelling/structured-text.md) fields.
-   When a record gets deleted, all the blocks it contains are deleted with it. This leaves no orphan data structures lying around your project.
    
-   Block fields per se cannot be localized. Instead, it's the containing Modular Content or Structured Text field that can be localized, so that different content/blocks can be defined for each language.
    

(Image content)

While link fields reference other records, Modular Content and Structured Text fields let you embed blocks inside the record

## When to use blocks instead of models?

It's fairly easy to recognize when a piece of content should be modeled as a model or block if you ask yourself the following questions:

-   *"Would I ever want to reference this content outside of the record in which it is defined?"* — if so, then it should be a model.
-   *"Does this content have standalone value, or does it make sense only in the context of a parent record?"* — in the first case, it should be a model; otherwise it should be a block.
    
-   *"If the parent record were to be deleted, do I want this content to be deleted as well, or would I like it to remain?"* — in the first case, it should be a block; otherwise it is a model.
    

#### Learn more about content modelling and blocks

Check out these video tutorials to get the best out of DatoCMS:

[

(Image content)

Intro to Blocks in DatoCMS

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-blocks-in-datocms.md)

[

(Image content)

Working Together - Let's Build Blocks!

Play video »

](https://www.datocms.com/user-guides/content-modeling/working-together-let-s-build-blocks.md)

[

(Image content)

Working Together - Enriching Content With Blocks

Play video »

](https://www.datocms.com/user-guides/content-management/working-together-enriching-content-with-blocks.md)

[

(Image content)

Working with nested blocks

Play video »

](https://youtu.be/AKkefmOZVJk)

[

(Image content)

Creating a Landing Page using the Atomic Design System

Play video »

](https://www.youtube.com/watch?v=rajqgvg2e0w)

---

# Content modelling — Modular content fields

Source [docs]: https://www.datocms.com/docs/content-modelling/modular-content.md

The **Modular Content** field is used to define a dynamic area for richer page layouts.

For example, in a landing page, defining a Modular Content field allows the writer to choose between adding a text section, a carousel, or a call-to-action. This gives the writer the freedom to compose a landing page by alternating and ordering as many of these choices as needed.

(Video content)

You can use Modular content to define dynamic layouts in any of your models: blog posts, landing pages, case studies, tutorials, or any place you want to give content writers a choice between different template options.

Developers are in charge of defining which elements writers can use to compose content for a specific modular content field. You can think of those as "low-level" models, called *Block models*. Authors, to compose their dynamic content, will be able to add and reorder these blocks as they prefer.

## How to build a Modular content editor

Suppose we have an *Article* model, and we want to add a modular content field to manage its content. The first step is to decide the different kinds of basic blocks you want your authors to alternate. In this case, we want our content to be a flexible composition of:

-   Text
-   Quotes
    
-   Videos
-   Text + Image blocks
    

To achieve this result, first, we create the Article model, and add a Modular content field to it:

(Image content)

In the*Validations* tab,you can choose which blocks will populate your modular content field. Let's add the Quote block:

(Image content)

## Create and edit a block

If you go to the *Blocks* tab in the Schema area, you will see all the blocks that you have already created, and you can create a new one:

(Image content)

Blocks are just a composition of fields, just like ordinary models. In our case, we want the *Quote Block* to be made of two fields: one containing the actual quote, and another containing the author.

You can click on the "Create new block" button on the bottom left to create a new block. In this case, we'll add a multi-paragraph text field to contain the text of the quote, and a single-line string text to display the name of the quote's author. If this block is used in one of your Models, you will see a notice. For example, we see that our *Quote* blockis used in the *Product* modular content field, which is part of the *Article* model.

(Image content)

If you go to your Content area now, you should see a new option called "Quote" in the modular content field's dropdown.

(Image content)

## Bulk Actions

Managing Modular Content is efficient with common Bulk Actions. You can easily select multiple Modular Content items and perform actions all at once from the action bar.

(Video content)

Each Modular Content Block includes a checkbox for easy selection, and you can perform bulk actions such as:

-   Select All / Invert Selection
-   Expand / Collapse selected blocks
    
-   Copy multiple blocks
-   Delete selected items
    

(Video content)

The contextual submenu makes managing blocks in the UI equally simple, with an improved flow to:

-   Copy & Paste
-   Duplicate
    
-   Move
-   Delete, and
    
-   Add Blocks
    

## Reusing block models

With these building blocks, you can start to design and develop a modular template that matches models and modular blocks in DatoCMS' schema.

Once you have set up the different blocks, you can reuse them across different models.

This means that the exact same block structure is reused across modular contents and models. If you modify the block in one place, the changes will be reflected across all modular contents.

This will effectively enable you to develop a modular template that will allow editors to build complex pages just by creating new records in the CMS.

## Single vs Multiple blocks

In your schema, there are two flavors of a Modular Content field you can opt for: Single Block and Multiple Blocks:

(Image content)

The Single Block allows authors to slot in just one block within the field, while the Multiple Blocks provides the flexibility to insert several. Hence, when you're fetching the value tied to modular content, you're either looking at an array of blocks or a single block.

In the case of Single Block, you can still allow the author to insert different types of blocks depending on the context, but always one at a time.

## Reusing fields across models with "Frameless" Single-block

As a project's complexity scales up, we frequently encounter the need to reuse subsets of fields across various models. Redundantly duplicating these fields or manually keeping them in sync isn't an appealing approach.

Let's say you have different content types like "Blog Post," "News Article," and "Product Review." Each of these models may have common fields like title, author, and tags. However, they will also have specific fields like "Body" for blog posts, "Summary" for news articles, and "Rating" for product reviews. Despite the differences, they all share a common structure with some overlapping fields.

In these scenarios, we can effectively leverage the reusability of block models coupled with the "frameless" display mode of the Modular Content (Single Block) field to achieve our goal.

First, let's create a new type of block model. Let's call it "Bloggable", and define all the shared fields within it:

(Image content)

At this point, in all the "Blog Post," "News Article," and "Product Review" models, we should incorporate a Modular Content (Single block) field. The field should be arranged as follows:

-   It should only have "Bloggable" as its associated block model;
-   It should have the Required validation active;
    
-   The "Frameless" presentation mode should be active.
    

(Video content)

The "Frameless" presentation mode will conceal the Modular Content field from the authoring interface, and only show the fields of the block model **as if they're an intrinsic part of the model itself**:

(Video content)

## Tutorials

If you're curious to see the full power of Modular Content fields in action, take a look at this video tutorials which covers everything you need to build a customizable landing page made of different reusable blocks.

[

(Image content)

Intro to the Modular Content Field

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-the-modular-content-field.md)

[

(Image content)

Building Pages and Deep Dive into Modular Content

Play video »

](https://www.datocms.com/user-guides/content-management/building-pages-and-deep-dive-into-modular-content.md)

[

(Image content)

Working Together - Creating Our First Case Study

Play video »

](https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-case-study.md)

[

(Image content)

Build a dynamic landing page with Next.js and Tailwind CSS

Play video »

](https://www.youtube.com/watch?v=it5nNneptgM)

---

# Content modelling — Structured text fields

Source [docs]: https://www.datocms.com/docs/content-modelling/structured-text.md

Structured Text is a field type that enables authors to **create rich text content**.

-   It offers a beautiful, Notion-like editor **designed for focus**, with slash commands, a full editing toolbar, markdown/keyboard shortcuts, and drag & drop functionality. Forget the mouse, and just start typing;
-   It allows you to create hyperlinks to other records in your project, and **intersperse textual content with custom blocks** - which can represent galleries, videos, embeds, call-to-actions, etc.
    
-   It stores the content in a safe, semantic, and readable **JSON format**, representing a tree of well-defined nodes.
    

## Backstory

Everyone hates HTML editors: developers know they produce dirty code, designers fear the introduction of unwanted styling, and editors struggle to use them. Markdown is better for designers, as it allows less freedom for editors from a formatting standpoint (at least until you start inserting HTML code), but it's not user friendly for editors, and it's an inflexible format for developers.

Sure, DatoCMS provides both an HTML and a Markdown editor, because there are situations where they're unavoidable, but often, when a project needs rich-text, **it is advisable to use Structured Text fields** instead.

## Preview of the editor

We designed the Structured Text editor to offer one of the best writing experiences on the market. It supports Slash commands, Markdown shortcuts, and full-screen focus mode. Here's a quick video of it:

(Video content)

For editors, familiarity with various content creation workflows is supported within the Structured Text field. In addition to slash commands, the floating formatting toolbar, and markdown support, the field also offers a full formatting toolbar visible when the field is in an active state.

(Video content)

The toolbar allows for advanced formatting options, as well as complete customisation with custom icons for plugins. Here is a brief look into all the available options to editors.

(Video content)

## Customizing the editor

A key aspect of Structured Text is the ability to customize the field so that authors are only exposed to relevant formatting options. For example, you can have fields with only certain header tags or limit the kinds of entries that can be hyperlinked or embedded:

(Image content)

To add custom blocks to the field, follow this short video:

(Video content)

Furthermore, you can enhance the field by adding custom icons into the Structured Text toolbar to interact with plugins and other customizations.

## Structured text on the API

Structured Text content is stored as a JSON object. We chose [unist](https://github.com/syntax-tree/unist) as our base format to benefit from its ecosystem of utilities for working with compliant syntax trees.

The `dast` format clearly specifies:

-   which nodes are usable within the document;
-   for each node, which are the possible `children` that it can contain;
    
-   any additional attribute that characterize each node.
    

Take a look at the [**DatoCMS Abstract Syntax Tree specs**](/docs/structured-text/dast.md) to learn all the details.

### Linking records

Structured Text allows hyperlinking DatoCMS records in the flow of text. This allows the following scenarios:

-   Using custom link functions, like React Router links, to a DatoCMS record.
-   Rendering a widget such as an image gallery, a product description box, a sign up form, an annotation window, or basically anything else.
    

The following example demonstrates an hyperlinked record and an inline record:

(Video content)

### Embedding blocks

Similarly to [Modular Content](/docs/content-modelling/modular-content.md) fields, you can also embed block records into Structured Text.

Blocks and records can be embedded either using slash commands or the toolbar. Here's a demonstration:

(Video content)

Just like with the Modular content field, when a record is deleted, the blocks contained inside its Structured Text fields are also deleted, without leaving orphans in the process.

### Next steps

-   [Structured Text format](/docs/structured-text/dast.md)
-   [Migrating to Structured Text](/docs/structured-text/migrating-content-to-structured-text.md)
    
-   [Creating Structured Text fields using the CMA](/docs/content-management-api/resources/field/create.md#creating-structured-text-fields)
-   [Creating records with Structured Text fields using the CMA](/docs/content-management-api/resources/item/create.md#structured-text-fields)
    
-   [Fetching Structured Text using the GraphQL CDA](/docs/content-delivery-api/structured-text-fields.md)
    

### Video tutorials

[

(Image content)

Intro to String (Text) Fields

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-string-text-fields.md)

[

(Image content)

Deep Dive into Structured Text in DatoCMS

Play video »

](https://www.datocms.com/user-guides/content-management/deep-dive-into-structured-text-in-datocms.md)

[

(Image content)

Working Together - Creating Our First Blog Post

Play video »

](https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-blog-post.md)

---

# Content modelling — Link fields

Source [docs]: https://www.datocms.com/docs/content-modelling/links.md

Links are a powerful way to model relationships between content. Models can have link fields which point to other records, for example:

-   An article linking to its category (singular relationship).
-   An article linking to related articles (plural relationship).
    

In DatoCMS, you don't need to define a field for the reverse relationship (i.e., the category linking to its articles): during the integration with your website, you can easily perform reverse reference lookups with just a couple of lines of code.

When you add a new field of type **Link** (or **Links**) to a model, DatoCMS requires you to specify within the *Validations* tab the models that can be referenced by the field itself.

To let editors select one (or more) records to link, DatoCMS will present a dropdown with auto-completion turned on:

(Video content)

### Expanded view

If you prefer, you can switch any link field to **Expanded view** mode, to provide your editors with a nicer, more meaningful preview of the linked records:

(Image content)

As with any other field, this setting can be found under the *Presentation* tab of your field:

(Image content)

[

(Image content)

Intro to the Link Field

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-the-link-field.md)

---

# Content modelling — SEO fields

Source [docs]: https://www.datocms.com/docs/content-modelling/seo-fields.md

If you are building a website, you need to think about SEO and provide special content for search engines and social networks.

To help content editors and marketers optimize your website, you can find a special "SEO and Social" type of field that lets you specify a custom title, description, image, and Twitter (X) card format, and gives a nice preview of how the result will look like on Google Search and Social Networks.

Here's how it works:

(Video content)

These are all the available fields:

-   **Title:** Customize the SEO title for your content.
-   **Description:** Craft a unique meta description to enhance search engine visibility.
    
-   **Image:** Set the featured image to be displayed in social previews.
-   **No Index:** Control whether the page should be indexed by search engines.
    
-   **Twitter (X) Card:** Fine-tune the appearance of shared content on X.
    

> [!PROTIP] Pro tip: Set SEO fallback
> You can set up fallback options for the SEO title and description for your models in case you don’t add SEO fields to a model or if your editors do not fill in the SEO fields. Just go to the Content area and click “SEO Preferences” in the main (left) sidebar.
> 
> See also: How SEO fallback works with the Content Delivery API is discussed here: [SEO and favicon fields](/docs/content-delivery-api/seo-and-favicon.md)

## Customizing SEO Fields and Social Link Previews

#### SEO Fields Customization

By navigating to *Edit field \> Presentation*, you can tailor the SEO fields that are displayed to editors, by selecting from the options. Select the fields that align with your editorial needs, providing a more focused and efficient editing environment.

#### Social Link Previews Customization

By navigating to *Edit field \> Presentation*, you can choose which social link previews to show your editors, ensuring your shared content looks compelling and engaging across various platforms. You can choose to display link previews for Google Search, X (Twitter), Facebook, Slack, Telegram and WhatsApp.

## Global SEO preferences

Also, globally, you can define a favicon for your site and a set of fallback meta for the site title, image, and description:

(Video content)

## API helpers

When fetching records from our GraphQL API, you'll find a `_seoMetaTags` helper which contains all the meta tags we offer, with the data already merged with the global SEO preferences and fallbacks.

You can read all the details in the relevant [section of the CDA docs](/docs/content-delivery-api/seo-and-favicon.md).

Want to know more about SEO customization in DatoCMS? Check out this video tutorials:

[

(Image content)

Understanding SEO in DatoCMS

Play video »

](https://www.datocms.com/user-guides/content-management/understanding-seo-in-datocms.md)

[

(Image content)

Intro to the SEO Fields

Play video »

](https://www.datocms.com/user-guides/content-modeling/intro-to-the-seo-fields.md)

[

(Image content)

Working with and customizing SEO Fields

Play video »

](https://youtu.be/WjF10isSjS0)

---

# Content modelling — Slugs and permalinks

Source [docs]: https://www.datocms.com/docs/content-modelling/slug-permalinks.md

In DatoCMS, you can add a special field type called "Slug" to your models to let your editors specify the URL permalink of a record.

A slug field is linked to another single-line string field of the same model, usually the title. As soon as the editor begins to type the title, the slug field will be filled with an URL-friendly version of the same string:

(Image content)

The nice thing about slug fields is that, if the editor subsequently updates the record's title, the slug won't change, preserving all the SEO benefits.

### How to add a slug field to a model

Say you have a "Blog post" model; start by adding a "Title" field, then you can add an automatically-generating Slug field (you can find it under the *SEO* group) by selecting the Title field as its reference, under the *Validations* tab.

(Video content)

---

# Content modelling — External video field

Source [docs]: https://www.datocms.com/docs/content-modelling/external-video-field.md

One of the fields that you can use in DatoCMS is the **external video field**, that allows you to reference an external YouTube, Vimeo or Facebook video.

Via [oEmbed](https://oembed.com/) we'll fetch and store the thumbnail image, the title and the dimensions of the video. All information that you can then retrieve via the APIs.

### How to publish a scheduled YouTube video

Unfortunately, oEmbed information can only be fetched from public videos, and not private videos with a scheduled publication date. This YouTube feature is very useful together with the [scheduled publication](https://www.datocms.com/blog/scheduled-publishing-during-christmas.md) of DatoCMS's records.

But how can you make the two work together?

There's a little trick that you can use, it's not super handy but will do the job:

-   set the video to unlisted, unfortunately you cannot schedule an unlisted video to be listed
-   add the video to DatoCMS
    
-   set the video to private again and schedule the publication
-   schedule the publication of DatoCMS record together with the YouTube video
    

That's it! A bit hackish, but it's a way to work around the limitations of the system.

---

# Content modelling — Validations

Source [docs]: https://www.datocms.com/docs/content-modelling/validations.md

Validations are a powerful tool to enforce a sound structure of your content.

They can help in different ways both editors and developers.

### For editors

If you have an editorial team with different people working on content, you need to explain to everyone what are the rules they have to respect for the final page to look good and make sense. Also sometimes developers need to enforce some content rules to make everything work together.

With DatoCMS **you can enforce all these rules on a per model/field basis**, preventing editors to save content that would break pages or make poor content.

For example, you can make certain fields as required, enforce certain text lengths and much more.

Enabling the "**Allow saving invalid drafts?**" flag on a per-model basis also allows editors to save invalid draft versions of records. In this case, validations will be enforced just right before publishing a record: if the record is not valid, it can't be published.

### For developers

When you work with complex structured or semi-structured data structures often you need to write frontend code that deals with all the possible combinations of existing/non-existing code or more in general you need to double check if content matches certain rules.

You end up with code that is much more complex than necessary, with lots of if-statements to protect you from unfinished content and parse and validate other parts to be sure you are getting what you need.

With DatoCMS you can simplify your code and be more productive. **By enforcing the right validations you'll always get the data that you need, in the format that you expect.**

Remember that **validations are normally enforced on every version of the record, even on saving a draft**. This means that if you won't be able to save a record that is not satisfying all the validations. So be careful adding only what you really need.

On each model, you can enable the "**Allow saving invalid drafts?" flag to postpone the enforcement of validations when publishing records. In this case, invalid records can be saved as drafts if the "draft/published" system is enabled.**

On the code side, using validations ensures that records are always published with the expected structure and format.

### Field validations

Let's see together all the validations available on DatoCMS for each field.

#### Single line text

-   *Required*: field must be present
-   *Unique*: every record of the same model must have different content
    
-   *Limit character count*: you can specify the number of characters in different ways, i.e. at least 10, between 10 and 20, no more than 20, exactly 20
-   *Match a specific pattern*: text must be a valid URL, email address or match a specified regular expression
    
-   *Accept only specified values*: you can specify a list of values. **If you do that, the field will display as a dropdown for the editor**
    

#### Multiple-paragraph text

-   *Required*: field must be present
-   *Limit character count*: you can specify the number of characters in different ways, e.g. at least 10, between 10 and 20, no more than 20, exactly 20
    
-   *Match a specific pattern*: text must be a valid URL, email address or match a specified regular expression
    

#### Modular content field

-   *Accept only a specified number of records*: you can specify the number of records part of the modular content in different ways, e.g. at least 10, between 10 and 20, no more than 20, exactly 20. Moreover you can specify if the number of records must be multiple of a number
    

#### Single asset field

-   *Required*: field must be present
-   *Accept only specified file size*: enforce a certain asset size in different ways, e.g. between 500KB and 1MB, no more than 10MB, at least 1MB
    
-   *Accept only specified extensions*: allow only images, videos, documents or custom file extensions
-   *Accept only specified image dimensions*: enforce dimensions for image assets, e.g. between 500x500px and 1000x1000px or no more than 2000x2000px or at least 500x500px
    
-   *Require alt and/or title*: you can enforce presence of alt and/or title fields
    

#### Asset gallery field

-   *Accept only a specified number of records*: you can specify the number of records part of the asset gallery in different ways, e.g. at least 10, between 10 and 20, no more than 20, exactly 20. Moreover you can specify if the number of records must be multiple of a number
-   *Accept only specified file size*: enforce a certain asset size in different ways, e.g. between 500KB and 1MB, no more than 10MB, at least 1MB
    
-   *Accept only specified extensions*: allow only images, videos, documents or custom file extensions
-   *Accept only specified image dimensions*: enforce dimensions for image assets, e.g. between 500x500px and 1000x1000px or no more than 2000x2000px or at least 500x500px
    
-   *Require alt and/or title*: you can enforce presence of alt and/or title fields
    

#### External video field

-   *Required*: field must be present
    

#### Date field

-   *Required*: field must be present
-   *Accept only specified date range*: the specified date must be in a specified range, e.g. at least 30 March 2020, no more than 21 March 2020, between 21 and 30 March 2020
    

#### DateTime field

-   *Required*: field must be present
-   *Accept only specified date range*: the specified date must be in a specified range, e.g. at least 30 March 2020 12:00, no more than 21 March 2020 18:00, between 21 12:00 and 30 March 2020 18:00
    

#### Integer number field

-   *Required*: field must be present
-   *Range*: number must be within specified range, e.g. between 1 and 10, at least 5, no more than 10
    

#### Boolean field

No validations available

#### Geolocation field

-   *Required*: field must be present
    

#### Color field

-   *Required*: field must be present
    

#### Slug field

-   *Reference field*: pick a field from which the slug is automatically pre-filled
-   *Required*: field must be present
    
-   *Unique*: every record of the same model must have different content
-   *Limit character count*: you can specify the number of characters in different ways, i.e. at least 10, between 10 and 20, no more than 20, exactly 20
    

#### SEO meta tags field

-   *Required*: field must be present
-   *Accept only specified file size*: enforce a certain asset size in different ways, e.g. between 500KB and 1MB, no more than 10MB, at least 1MB
    
-   *Accept only specified image dimensions*: enforce dimensions for image assets, e.g. between 500x500px and 1000x1000px or no more than 2000x2000px or at least 500x500px
    

#### Single link field

-   *Accept only specified model*: pick one or more models from which you are allowed to pick links
-   *Required*: field must be present
    
-   *Unique*: every record of the same model must have different content
    

#### Multiple links field

-   *Accept only specified model*: pick one or more models from which you are allowed to pick links
-   *Accept only a specified number of records*: you can specify the number of links in different ways, e.g. at least 10, between 10 and 20, no more than 20, exactly 20. Moreover you can specify if the number of records must be multiple of a number
    

#### JSON field

-   *Required*: field must be present

---

# Content modelling — Data consistency: key concepts and implications

Source [docs]: https://www.datocms.com/docs/content-modelling/data-migration.md

In DatoCMS, you are free to edit your project schema at any time. While this is great news for you, it also complicates the situation quite a bit on our part!

Suppose you have an *Article* model, and you already have a number of articles stored. What happens to these existing articles in one of the following situations?

-   You add a new mandatory field.
-   You transform a non-localized field into a localized one (or vice versa).
    
-   You add a new locale in your project settings.
    

Well, the existing articles (including those already published) suddenly become invalid: the data they contain does not comply with the new schema.

In this section, we will try to explore together how DatoCMS manages these and other similar cases. To avoid simply having an endless list of unclear rules, we will start by explaining the mental model that underlies these rules, so that hopefully they will become more intuitive.

### How DatoCMS internally stores your content: a mental model

This is a simplified version of the mental model to keep in mind when working with DatoCMS:

(Image content)

The record's meta-information, like creation date, publication date, record creator, etc.. is stored directly at the record level. The record also contains all the details about its location in the collection, whether it's for [simple](/docs/content-modelling/record-ordering.md) or [tree-like sorting](/docs/content-modelling/hierarchical-sorting.md).

However, the actual value of the record's fields are versioned, allowing the history of the record's changes over time to be tracked.You can think of these versions as being in a separate table, connected to the associated record.

In addition to field values, every version also keeps track of the editor who made the changes, and whether the data is valid or not, based on the compliance with the model's latest field-level validation rules.

##### Current and published versions

Out of all the historical record versions, two are particularly important: the **current version** and the **published version**:

-   The current version represents the **latest available version**: every time a record is updated, a new version is generated and marked as the new current version.
-   The published version represents the version **currently marked as published**. It might coincide with the current version, or it might not. It might also not exist at all!
    

##### The status of a record

The status of a record precisely represents the relationship between its current and published versions:

-   Record is **in draft**: only has the current version, and no published version;
-   Record is **published**: current version and published version coincide;
    
-   Record is **updated**: record has both current and published versions, but they differ.
    

### How our APIs expose this data structure

All our APIs have been designed to be pragmatic and simplify the lives of developers by hiding some of this complexity. How?

When you're pulling data about records through an API, you have the option to specify whether you're referring to the current version or the published version of your records (if you're not actively doing this, a default is implicitly applied):

-   With the [Content Delivery API](/docs/content-delivery-api.md) and the [Realtime Updates API](/docs/real-time-updates-api.md), the default is to consider the published versions, but you can request to consider the current versions with the header [`X-Include-Drafts: true`](/docs/content-delivery-api/api-endpoints.md#preview-mode-to-retrieve-draft-contenthttps://www.datocms.com/docs/content-delivery-api/api-endpoints#preview-mode-to-retrieve-draft-content).
-   With the [Content Management API](/docs/content-management-api.md), you can request to consider the published version or the current version with the parameter [`?version=current`](/docs/content-management-api/resources/item/instances.md) or [`?version=published`](/docs/content-management-api/resources/item/instances.md).
    

With this information at hand, all APIs can now represent a record as a single entity, encompassing both the meta-information present at the record level, and the model fields data that is present at the version level. This greatly simplifies the logic of 99% of web projects that interface with DatoCMS, which can therefore work considering a single entity instead of two.

What if a specific record does not have a published version, and the APIs are requested to refer to the published versions? Then that record simply won't be retrieved, as if it doesn't exist — which is exactly how one would normally want to handle this type of case on the app side.

> [!POSITIVE] With CMA, you can also access all other past versions
> We've optimized our system to mainly work with the current and published versions of a record, as these are typically the ones of interest. However, our Content Management API can also [return the full version history of a record](/docs/content-management-api/resources/item-version/instances.md) if needed!

### Data consistency rules guaranteed by the system

DatoCMS maintains two important guarantees:

-   The structure of the data contained in any version of a record (even past versions) is guaranteed to be consistent with the settings of its model and fields.
-   The validity of a current/published version always reflects the current validation rules.
    

### Consequences on the published and current version of a record

It is crucial to understand a significant outcome of these guarantees and data setup: there are cases where **the published version can change without a specific "publish" action** on the record, and **the current version can be modified without a distinct "update" action:**

-   Changes in the sort order of a record in the collection are immediately reflected online: it is not possible to keep these changes "in draft" because they are information that live directly at the record level. When the position is changed, the "published" version will also display the updated information. The same applies to other meta-information: creator, creation/publication dates, etc.
-   There are situations where a change to a record/asset can have repercussions on the published and current versions of other records that reference them:
    
    -   Imagine a record whose current or published version references an asset in the Media Area, and the field that contains it has validations (i.e., "the asset must be an image"). If the asset is subsequently modified, replacing the asset with a new file, the new file could potentially alter the validity status of the current or published version, which is therefore updated.
        
    -   Imagine a record whose current or published version references another record via a Single Link, Multiple Links, or Structured Text field:
        
        -   If the field has the setting "When deletion is requested for a record referenced by this field" set to "Try to remove the reference to the deleted record", then the system must respect this setting, altering the current and/or published version.
            
        -   Similarly, if the field has the setting "When unpublishing is requested for a record referenced by this field" set to "Try to remove the reference to the unpublished record", then the system must respect this setting, altering the published version.
            
-   Changing the schema of a model, or the locales of a DatoCMS project can also cause an automatic update of multiple versions because:
    
    -   When a new field is added/removed to the model, this field will be immediately added/eliminated in all record versions of that model (including the published and current version).
        
    -   If a field that can hold a reference to another record (Single Link, Multiple Links, Structured Text) is altered by removing a model from the list of linkable models, then all record versions of that model will be updated by eliminating any references to those models.
        
    -   If a model is deleted, but there are records of that model which are referenced by other records, then all these record versions are updated by eliminating any references to the deleted model.
        
    -   The same principle applies to model fields that can hold blocks (Modular Content, Structured Text). If these are altered by removing a block type from the list of embeddable options, then all record versions of that model will be adjusted by eliminating any blocks of that type.
        
    -   If a model is modified, enabling the *"All locales required?"* setting, then all previously unspecified locales will be added to all record versions of that model.
        
    -   If a field is modified from localized to non-localized (or vice versa), then all record versions of that model will be modified to reflect this change.
        
    -   If you add a new validation rule to a field, then all existing record versions of that model will be re-checked against the new validation rules, and potentially marked as invalid.
        
    -   If a locale is added/removed from the project, all record versions of all models that contain localized fields will be adjusted accordingly.
        

### Consequences in Webhooks

Webhooks allow you to be notified of changes to the records in your project. Based on the considerations made so far, it is important to make a few clarifications here:

-   The **"Record creation"** event is triggered when a record is generated for the first time (and consequently its current version).
-   The **"Record update"** event is triggered when the current version changes (due to an explicit modification of the record, or for some of the reasons listed above).
    
-   The **"Record publish"** event is triggered when the published version of a record changes (due to an explicit publication of the record, or for some of the reasons listed above).
-   In the webhook payload, the `meta.status` field of the record entity always reflects the relationship between the current and published versions of the item itself at the moment the webhook is triggered.
    

As a consequence:

-   You can still get "Record publish"/"Record update" events without an explicit new publish/update request from an editor or an API call. This occurs when the system automatically needs to adjusts an existing published/current version to keep it consistent with the new schema change.
-   When these automatic adjustments occur, it is completely normal for the `meta.status` of a record in the webhook payload of a "Record Publish" event to be "updated" instead of "published". This is because during the process, a record might be in an "updated" state, and the operation does not change this condition.

---

# DatoCMS APIs at a glance — Overview of DatoCMS APIs

Source [docs]: https://www.datocms.com/docs/overview/overview-of-datocms-apis.md

DatoCMS offers several different APIs, each optimized for a particular use case.

## TLDR

DatoCMS provides you 4 APIs to build with, each one documented in detail over the following links:

-   [GraphQL Content Delivery API](/docs/content-delivery-api.md) to read published content from any frontend via a fast, cached GraphQL endpoint,
-   [REST Content Management API](/docs/content-management-api.md) to create, update, and publish records programmatically through a typed client,
    
-   An [Asset API](/docs/asset-api/images.md) to upload, transform, and serve images and videos from a global CDN with inbuilt optimization settings and options, and
-   An [SSE Real-time Updates API](/docs/real-time-updates-api.md) to stream live content changes to power instant previews and live published views.
    

## Fetching & Serving Content for your Frontend

-   **Start here:** Our [**Content** **Delivery** **API**](/docs/content-delivery-api.md) (CDA) is our recommended way to connect DatoCMS to your frontend. It lets you retrieve only the records and fields you need, using a simple query language called [GraphQL](https://graphql.org/). This is a fast and safe read-only API that makes it easy to use our headless system with any frontend framework.
-   Our [**Site Search API**](/docs/site-search.md)lets you easily add full-text search to your website.
    
-   Our[**Real-Time Updates API**](/docs/real-time-updates-api.md) allows you to push live updates to your visitors for real-time blogging or other live events. It is also highly beneficial for previewing draft content to your editors as they compose.
    

## Serving Images & Videos

-   Our [**Images API**](/docs/asset-api/images.md)serves your images through a CDN and enables powerful URL-based transformations ([cropping, resizing, format conversion, and more](https://docs.imgix.com/apis/rendering/overview)). We partner with Imgix for this system.
-   Our [**Videos API**](/docs/asset-api/videos.md) uses the Mux video CDN to [ensure efficient video streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md) for users with different devices and connection speeds.
    

## Editing & Managing Your Data

Our [**Content Management API**](/docs/content-management-api.md) (CMA) is a traditional REST API that lets your developers create, edit, export, and import your data and schema.

This is also the API that lets you manage other aspects of your account and projects, such as roles & permissions, environments, collaborators, and more.

## Extending DatoCMS

The [**DatoCMS Plugins SDK**](/docs/plugin-sdk/introduction.md) (and associated API methods) let your developers customize the DatoCMS UI itself, extending functionality for your editors by easily integrating third-party services or adding special logic for your specific business needs.

See [DatoCMS Community Plugins](https://www.datocms.com/marketplace/plugins.md) for some examples, often open-source, built by our wonderful community.

---

# DatoCMS APIs at a glance — DatoCMS Domains and Content Security Policy (CSP)

Source [docs]: https://www.datocms.com/docs/overview/datocms-domains-and-content-security-policy-csp.md

**Last updated: 2026-01-12**

If you're trying to whitelist our domain names for the purposes of browser [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) or similar needs, this is a list of the domains used by our services.

## For API requests:

-   [`graphql.datocms.com`](http://graphql.datocms.com/) (Content Delivery API, GraphQL)
-   [`graphql-listen.datocms.com`](http://graphql-listen.datocms.com/) (Real-time Updates API for live previews via SSE)
    
-   [`site-api.datocms.com`](http://site-api.datocms.com/) (Content Management API, REST)
    

## For images and assets:

-   [`www.datocms-assets.com`](http://www.datocms-assets.com/) (primary CDN for all assets like images, PDFs, raw files)
-   [`datocms-assets.6c36efb897e5eae1d2a887cfa632eea9.eu.r2.cloudflarestorage.com`](https://datocms-assets.6c36efb897e5eae1d2a887cfa632eea9.eu.r2.cloudflarestorage.com/) (for uploading assets to your project)
    

## For HLS video streaming:

-   [`stream.mux.com`](http://stream.mux.com/) (video streaming via HLS and MP4 delivery)
-   [`image.mux.com`](http://image.mux.com/) (video thumbnails and metadata)
    

## Other

-   If you’re embedding the DatoCMS admin interface or using plugins: `*.admin.datocms.com` (the CMS editor interface).
-   Your plugin is likely hosted elsewhere, outside of DatoCMS altogether, like a Vercel or Netlify site.
    
-   If you’re on an Enterprise plan with a custom asset domain, you’d replace [`www.datocms-assets.com`](http://www.datocms-assets.com/) with your custom domain. The same applies if you use a custom CMS admin domain.
    

## For humans only

These sites are unlikely to be useful to APIs, but you may wish to whitelist them for your human users.

-   Our forum at [`https://community.datocms.com`](https://community.datocms.com/) is helpful for troubleshooting
-   Your account dashboard is at [`https://dashboard.datocms.com`](https://dashboard.datocms.com/)

---

# Content Delivery API — Content Delivery API Overview

Source [docs]: https://www.datocms.com/docs/content-delivery-api.md

This section offers a detailed reference to DatoCMS's Content Delivery API.

The Content Delivery API is used to retrieve content from one of your DatoCMS projects and deliver it to your web or mobile projects.

Our APIs serve content via a powerful and robust content delivery network (CDN). Multiple data centers around the world store a cached copy of your content. When a page request is made, the content is delivered to the user from the nearest server. This greatly accelerates content delivery and reduces latency.

> [!NOTE] Content Delivery vs Content Management API
> If you need to deliver content to your public-facing web or mobile projects, this is the API to use, while if you want to programmatically create or update your schema/content, please refer to the [Content Management API](/docs/content-management-api.md)!

### Why GraphQL?

The Content Delivery API is written in GraphQL, which offers a number of advantages over classic REST APIs:

#### Strongly typed schema

Many developers have found themselves in situations where they needed to work with deprecated API documentation, lacking proper ways of knowing what operations are supported by an API and how to use them. GraphQL clearly defines the operations supported by the API, including input arguments and possible responses, offering an unfailing contract that specifies the capabilities of an API.

#### No more over-fetching and under-fetching

Developers often describe the major benefit of GraphQL as the fact that clients can retrieve exactly the data they need from the API. They don’t have to rely on REST endpoints that return predefined and fixed data structures. Instead, the client can dictate the shape of the response objects returned by the API.

#### Fewer roundtrips

One of the major issues of REST is that, in order to get the data you need, you are forced to call a number of different endpoints. Each API request to pull a resource is a separate HTTP request-response cycle. Fetching complicated data requires multiple round-trips between the client and server to render even a single view. On the contrary, GraphQL enables you to call several related functions without multiple round-trips.

> [!PROTIP] Pro tip: DatoCMS, powered by DatoCMS
> Of course, we drink our own champagne - our website is built on DatoCMS.
> 
> Want a peek behind the curtain? The actual source code is available in this [public GitHub repo](https://github.com/datocms/astro-website) for you to explore and see how we built it.

### Want to get started with DatoCMS?

If you are new to DatoCMS and you want to learn the basics, check these video tutorials for beginners!

[

(Image content)

Next.js + DatoCMS tutorial for beginners

Play video »

](https://www.youtube.com/watch?v=_VIF1if-dNA)

[

(Image content)

Build a dynamic landing page with Next.js and Tailwind CSS

Play video »

](https://www.youtube.com/watch?v=it5nNneptgM)

[

(Image content)

Creating a Landing Page using the Atomic Design System

Play video »

](https://www.youtube.com/watch?v=rajqgvg2e0w)

---

# Content Delivery API — Using the JavaScript CDA client

Source [docs]: https://www.datocms.com/docs/content-delivery-api/your-first-request.md

The DatoCMS Content Delivery API is a single GraphQL endpoint:

```plaintext
https://graphql.datocms.com/
```

The endpoint stays constant no matter what operation you perform, and it's read-only — that is, it does not offer any *mutation operation*. You can use our [Content Management API](/docs/content-management-api.md) for that.

Unlike REST, the HTTP verb does not vary by operation: every request — whether you’re querying or fetching with variables — is a `POST` to that URL, with a JSON-encoded body.

### Making a raw HTTP request

You can talk to the API from anything that speaks HTTP. Here's the same query sent with `curl` and with the native Fetch API:

cURL

Terminal window

```bash
$ curl 'https://graphql.datocms.com/' \
    -H 'Authorization: Bearer YOUR-API-TOKEN' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    --data-binary '{ "query": "{ allPosts { title } }" }'
```

Vanilla JS (Fetch API)

```javascript
const response = await fetch('https://graphql.datocms.com/', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': `Bearer ${process.env.DATOCMS_READONLY_TOKEN}`,
  },
  body: JSON.stringify({
    query: '{ allPosts { title } }',
  }),
});

const { data } = await response.json();
console.log(data);
```

This works, but you'll quickly need to layer extra concerns on top: rate-limit retries, draft/preview mode, environment targeting, cache tags, pagination beyond the 500-record limit, and TypeScript types. That's exactly what our official client takes care of.

### Using the `@datocms/cda-client` package

`@datocms/cda-client` is a lightweight, TypeScript-first package that wraps the native Fetch API with helpers tailored for the Content Delivery API. It's the recommended way to query DatoCMS from any JavaScript or TypeScript runtime — Node.js, browsers, edge runtimes, Deno, Bun.

It offers a number of benefits over making raw requests yourself:

-   **The package is written in TypeScript**, with full support for `TypedDocumentNode` so that — paired with [gql.tada](https://gql-tada.0no.co/) or [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) — you get end-to-end type inference on every query result and variable;
-   All the [API headers](/docs/content-delivery-api/api-endpoints.md) that control environments, draft mode, strict mode, cache tags and Content Link are exposed as **plain options on the client**, no manual header juggling required;
    
-   API rate-limit retries are managed for you automatically, and a dedicated helper [bypasses the API's 500-record per-query limit](/docs/content-delivery-api/pagination.md) transparently.
    

###### Installation

Terminal window

```bash
npm install @datocms/cda-client
```

The package has zero runtime dependencies and works in any environment that ships a native Fetch API (Node.js ≥ 18, modern browsers, Cloudflare Workers, Vercel Edge Functions, Deno, Bun, etc.).

###### Executing a query

The main entry point is `executeQuery`. It accepts a GraphQL query (as a string, `DocumentNode`, or `TypedDocumentNode`) and an options object, and returns a Promise that resolves with the query result:

```typescript
import { executeQuery } from '@datocms/cda-client';

const result = await executeQuery('{ allPosts { title } }', {
  token: process.env.DATOCMS_READONLY_TOKEN,
});

console.log(result);
```

You can pass GraphQL variables under the `variables` option:

```typescript
const result = await executeQuery(
  `query PostsByCategory($category: String!) {
    allPosts(filter: { category: { eq: $category } }) {
      title
    }
  }`,
  {
    token: process.env.DATOCMS_READONLY_TOKEN,
    variables: { category: 'news' },
  },
);
```

### TypeScript and end-to-end type safety

`executeQuery` supports `TypedDocumentNode`, so the type of the response is inferred directly from your query. Combined with a code generator, you get autocomplete and compile-time checks on every field:

```typescript
import { executeQuery } from '@datocms/cda-client';
import { AllArticlesQuery } from './generated/graphql';

const result = await executeQuery(AllArticlesQuery, {
  token: process.env.DATOCMS_READONLY_TOKEN,
  variables: { limit: 10 },
});

// `result.allArticles` is fully typed
result.allArticles.forEach((article) => console.log(article.title));
```

### Error management

If a request fails — either because of a non-2xx HTTP status, or because the GraphQL response contains an `errors` array — the client throws an `ApiError` exception containing all the details of the request and the response:

```typescript
import { executeQuery, ApiError } from '@datocms/cda-client';

try {
  const result = await executeQuery('{ allPosts { title } }', {
    token: process.env.DATOCMS_READONLY_TOKEN,
  });
  console.log(result);
} catch (e) {
  if (e instanceof ApiError) {
    // Information about the failed request
    console.log(e.query);
    console.log(e.options);

    // Information about the response
    console.log(e.response.status);
    console.log(e.response.statusText);
    console.log(e.response.headers);
    console.log(e.response.body);
  } else {
    throw e;
  }
}
```

You can learn more about the package and explore its full API surface — including `rawExecuteQuery`, `buildRequestHeaders` and `buildRequestInit` for framework integrations — on the [`@datocms/cda-client` README](https://github.com/datocms/cda-client).

---

# Content Delivery API — Authentication and permissions

Source [docs]: https://www.datocms.com/docs/content-delivery-api/authentication.md

The Content Delivery API uses API Tokens for authentication:

@datocms/cda-client

```typescript
import { executeQuery } from '@datocms/cda-client';

const result = await executeQuery(query, {
  token: process.env.DATOCMS_READONLY_TOKEN,
});
```

Raw HTTP header

```plaintext
Authorization: Bearer <YOUR-API-TOKEN>
```

You can find your read-only API token in the *Settings \> API tokens* section of your administrative area, or generate a new token with more specific permissions:

(Video content)

Regardless of which API token you use, make sure that the "Access the Content Delivery API" or "Access the Content Delivery API in Preview Mode" flags are enabled, otherwise the API token will not be able to make calls to the CDA.

#### Restricting access

If you want to restrict GraphQL access only to a selection of your models, you can generate a custom API token and assign it a custom [role](/docs/general-concepts/roles-and-permission-system.md).

(Video content)

If an API token can only access specific models, any other field **will be completely hidden from the GraphQL schema and response**, eliminating any potential information exposure.

> [!WARNING] Different behavior on legacy projects
> On projects created before January 8, 2024 — and that have not explicitly activated the "Improved GraphQL Security" update — the behavior will be slightly different: you can read all the details in the related [product update](https://www.datocms.com/product-updates/improved-gql-visibility-control.md).

---

# Content Delivery API — Configuring requests: envs, drafts, strict mode, cache tags, etc.

Source [docs]: https://www.datocms.com/docs/content-delivery-api/api-endpoints.md

Each request to the GraphQL endpoint can carry **headers** that control behavior: target a specific environment, opt into draft content, narrow down GraphQL types, retrieve cache tags, and embed visual-editing metadata. Below, each section shows the raw HTTP header alongside the equivalent option on [`@datocms/cda-client`](/docs/content-delivery-api/your-first-request.md), so you can pick whichever style fits your setup.

### Specifying an environment

If no environment is specified, the [primary environment](/docs/general-concepts/primary-and-sandbox-environments.md) is used. To explicitly read from a different one:

@datocms/cda-client

```typescript
import { executeQuery } from '@datocms/cda-client';

const result = await executeQuery(query, {
  token: process.env.DATOCMS_READONLY_TOKEN,
  environment: 'my-sandbox-environment',
});
```

Raw HTTP header

```plaintext
X-Environment: <ENVIRONMENT-NAME>
```

### Preview mode to retrieve draft content

If you have the [Draft/Published system](/docs/general-concepts/draft-published.md) active on some of your models, you can request the latest draft version of each record instead of the currently published one — useful for staging environments and local development:

@datocms/cda-client

```typescript
const result = await executeQuery(query, {
  token: process.env.DATOCMS_PREVIEW_TOKEN,
  includeDrafts: true,
});
```

Raw HTTP header

```plaintext
X-Include-Drafts: true
```

### Strict mode for non-nullable GraphQL types

If you want to make sure that no invalid record is ever returned without having to manually add an `_isValid` filter to every query, you can opt into strict mode:

@datocms/cda-client

```typescript
const result = await executeQuery(query, {
  token: process.env.DATOCMS_READONLY_TOKEN,
  excludeInvalid: true,
});
```

Raw HTTP header

```plaintext
X-Exclude-Invalid: true
```

In contrast to the `_isValid` filter, this header also has the effect of **narrowing down GraphQL types**:

-   Every field with a "Required" validation enforced will be associated with a non-nullable GraphQL type (e.g. `String` becomes `String!`)
-   Asset fields (both single and multiple) that have a "Image transformable by imgix" format validation will have the following properties associated with a non-nullable type: `focalPoint`, `width`, `height`, `responsiveImage`
    
-   Asset fields (both single and multiple) that have a "Video" format validation will have the following property associated with a non-nullable type: `video`
-   Asset fields (both single and multiple) that have a required alt and/or title validation will have the `alt` and/or `title` properties marked as non-null
    

You can read a little bit more about Strict Mode in our [announcement blog post](https://www.datocms.com/blog/introducing-strict-mode-for-graphql-cda-get-the-best-typescript-dx.md).

### Cache tags

To receive the [Cache Tags](/docs/content-delivery-api/cache-tags.md) associated with your query, opt into the dedicated header. Cache tags travel back in the `X-Cache-Tags` response header, so on the client side you'll need access to the underlying `Response` — `rawExecuteQuery` returns it alongside the parsed result:

@datocms/cda-client

```typescript
import { rawExecuteQuery } from '@datocms/cda-client';

const [result, response] = await rawExecuteQuery(query, {
  token: process.env.DATOCMS_READONLY_TOKEN,
  returnCacheTags: true,
});

const cacheTags = response.headers.get('x-cache-tags');
```

Raw HTTP header

```plaintext
X-Cache-Tags: true
```

### Content Link

If you have [Content Link](/docs/general-concepts/visual-editing.md) available on your project, two extra headers tell the CDA to embed the metadata that powers visual editing on websites hosted on Vercel:

@datocms/cda-client

```typescript
const result = await executeQuery(query, {
  token: process.env.DATOCMS_READONLY_TOKEN,
  contentLink: 'v1',
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
```

Raw HTTP headers

```plaintext
X-Visual-Editing: v1
X-Base-Editing-Url: https://<YOUR-PROJECT-NAME>.admin.datocms.com
```

`X-Base-Editing-Url` (and the equivalent `baseEditingUrl` option) can also be used in isolation: it enables the usage of the `_editingUrl` field in the GraphQL API.

---

# Content Delivery API — How to fetch records

Source [docs]: https://www.datocms.com/docs/content-delivery-api/how-to-fetch-records.md

### Query a single record

For every model there is one query to fetch a specific record. For example if you want to get the `hero_title` field from the single-instance model called `homepage`, the following request can be used:

```graphql
query {
  homepage {
    heroTitle
  }
}
```

The query response can be further controlled by supplying `filter` and `orderBy` arguments. For example, if the `artist` model has a `name` field, you can use this query to get a specific record:

```graphql
query {
  artist(filter: { name: { eq: "Blank Banshee" } }) {
    name
    genre
  }
}
```

Please refer to the [filtering section](/docs/content-delivery-api/filtering-records.md) of this guide to understand how to use the `filters` and `orderBy` arguments.

### Query multiple records

The API contains automatically generated queries to fetch records of a certain model. For example, for the `artist` model the top-level query `allArtists` will be generated. By default the query will return 20 records, you can change the limit by adding the `first` parameter. If the number of your records exceeds the maximum number of records we return, you will need to iterate. Read more in the [pagination section](/docs/content-delivery-api/pagination.md).

A few examples for query names:

-   Model API identifier: `artist`, query name: `allArtists`
-   Model API identifier: `track`, query name: `allTracks`
    
-   Model API identifier: `use_case`, query name: `allUseCases`
    

A query which fetches the first ten records from the `artist` model — together with the total number of artists — could look like the following:

```graphql
query {
  allArtists(first: 10) {
    id
    name
  }
  _allArtistsMeta {
    count
  }
}
```

Note: The query name approximates the plural rules of the English language. If you are unsure about the actual query name, explore available queries in your CDA Playground.

The query response of a query fetching multiple records can be further controlled by supplying different query arguments to order, filter and paginate results.

---

# Content Delivery API — Filtering records

Source [docs]: https://www.datocms.com/docs/content-delivery-api/filtering-records.md

You can supply different parameters to the `filter` argument to filter the query response accordingly. The available options depend on the fields defined on the model in question.

If you supply exactly one parameter to the filter argument, the query response will only contain records that fulfill this constraint:

```graphql
query {
  allArtists(
    filter: {
      published: { eq: false }
    }
  ) {
    id
    name
    published
  }
}
```

Depending on the type of the field you want to filter by, you have access to different advanced criteria you can use to filter your query response:

```graphql
query {
  allArtists(
    filter: {
      name: { in: [ "Blank Banshee", "Gazelle Twin" ] }
    }
  ) {
    id
    name
    genre
  }
}
```

If you specify multiple conditions, they will be combined as if it was a logical AND expression:

```graphql
query {
  allAlbums(
    filter: {
      { artist: { eq: "212" } },
      { releaseDate: { gt: "2016-01-01" } }
    }
  ) {
    id
    slug
    artist { name }
    coverImage { url }
  }
}
```

There are times where it can be more convenient to use an AND expression explicitly, for example when you need to use the same type of filter more than once:

```graphql
query {
  allArtists(
    filter: {
      AND: [
        { name: { matches: { pattern: "Blank" } } },
        { name: { matches: { pattern: "Banshee" } } }
      ]
    }
  ) {
    id
    name
    genre
  }
}
```

It is also possible to combine AND-like and OR logical expressions. For example, the following query will return all the point of interest located in New York that either have a rating greater than 4 or are a restaurant:

```graphql
query {
  allPois(
    filter: {
      address: { matches: { pattern: "new york" } },
      OR: [
        { rating: { gt: 4 } },
        { name: { matches: { pattern: "restaurant" } } },
      ]
    }
  ) {
    name
    address
    rating
  }
}
```

> [!WARNING] Structured Text and Deep Filtering
> If a Structured Text field has the [deep filtering](/docs/content-delivery-api/deep-filtering.md) option enabled, its filters will slightly differ from the ones described in this page. You learn more in the next section of the doc regarding [deep filtering](/docs/content-delivery-api/deep-filtering.md).

## Filters available for field types

#### Boolean fields

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { booleanField: { eq: true } }) {
    title
  }
}
```

#### Color fields

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { colorField: { exists: true } }) {
    title
  }
}
```

#### Date fields

`gt`

Filter records with a value that's strictly greater than the one specified

```graphql
query {
  allProducts(filter: { dateField: { gt: "2018-02-13" } }) {
    title
  }
}
```

`lt`

Filter records with a value that's less than the one specified

```graphql
query {
  allProducts(filter: { dateField: { lt: "2018-02-13" } }) {
    title
  }
}
```

`gte`

Filter records with a value that's greater than or equal to the one specified

```graphql
query {
  allProducts(filter: { dateField: { gte: "2018-02-13" } }) {
    title
  }
}
```

`lte`

Filter records with a value that's less or equal than the one specified

```graphql
query {
  allProducts(filter: { dateField: { lte: "2018-02-13" } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { dateField: { exists: true } }) {
    title
  }
}
```

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { dateField: { eq: "2018-02-13" } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match

```graphql
query {
  allProducts(filter: { dateField: { neq: "2018-02-13" } }) {
    title
  }
}
```

#### DateTime fields

`gt`

Filter records with a value that's strictly greater than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      dateTimeField: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter records with a value that's less than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      dateTimeField: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter records with a value that's greater than or equal to than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      dateTimeField: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter records with a value that's less or equal than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      dateTimeField: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`eq`

Filter records with a value that's within the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      dateTimeField: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Filter records with a value that's outside the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      dateTimeField: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { dateTimeField: { exists: true } }) {
    title
  }
}
```

#### Single file fields

`eq`

Search for records with an exact match. The specified value must be an Upload ID

```graphql
query {
  allProducts(filter: { fileField: { eq: "123" } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match. The specified value must be an Upload ID

```graphql
query {
  allProducts(filter: { fileField: { neq: "123" } }) {
    title
  }
}
```

`in`

Filter records that have one of the specified uploads

```graphql
query {
  allProducts(filter: { fileField: { in: ["123"] } }) {
    title
  }
}
```

`not_in`

Filter records that do not have one of the specified uploads

```graphql
query {
  allProducts(filter: { fileField: { notIn: ["123"] } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { fileField: { exists: true } }) {
    title
  }
}
```

#### Floating-point number fields

`gt`

Filter records with a value that's strictly greater than the one specified

```graphql
query {
  allProducts(filter: { floatField: { gt: 19.99 } }) {
    title
  }
}
```

`lt`

Filter records with a value that's less than the one specified

```graphql
query {
  allProducts(filter: { floatField: { lt: 19.99 } }) {
    title
  }
}
```

`gte`

Filter records with a value that's greater than or equal to the one specified

```graphql
query {
  allProducts(filter: { floatField: { gte: 19.99 } }) {
    title
  }
}
```

`lte`

Filter records with a value that's less or equal than the one specified

```graphql
query {
  allProducts(filter: { floatField: { lte: 19.99 } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { floatField: { exists: true } }) {
    title
  }
}
```

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { floatField: { eq: 19.99 } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match

```graphql
query {
  allProducts(filter: { floatField: { neq: 19.99 } }) {
    title
  }
}
```

#### Multiple files fields

`eq`

Search for records with an exact match. The specified values must be Upload IDs

```graphql
query {
  allProducts(filter: { galleryField: { eq: ["123"] } }) {
    title
  }
}
```

`all_in`

Filter records that have all of the specified uploads. The specified values must be Upload IDs

```graphql
query {
  allProducts(filter: { galleryField: { allIn: ["123"] } }) {
    title
  }
}
```

`any_in`

Filter records that have one of the specified uploads. The specified values must be Upload IDs

```graphql
query {
  allProducts(filter: { galleryField: { anyIn: ["123"] } }) {
    title
  }
}
```

`not_in`

Filter records that do not have any of the specified uploads. The specified values must be Upload IDs

```graphql
query {
  allProducts(filter: { galleryField: { notIn: ["123"] } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { galleryField: { exists: true } }) {
    title
  }
}
```

#### Integer number fields

`gt`

Filter records with a value that's strictly greater than the one specified

```graphql
query {
  allProducts(filter: { integerField: { gt: 3 } }) {
    title
  }
}
```

`lt`

Filter records with a value that's less than the one specified

```graphql
query {
  allProducts(filter: { integerField: { lt: 3 } }) {
    title
  }
}
```

`gte`

Filter records with a value that's greater than or equal to the one specified

```graphql
query {
  allProducts(filter: { integerField: { gte: 3 } }) {
    title
  }
}
```

`lte`

Filter records with a value that's less or equal than the one specified

```graphql
query {
  allProducts(filter: { integerField: { lte: 3 } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { integerField: { exists: true } }) {
    title
  }
}
```

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { integerField: { eq: 3 } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match

```graphql
query {
  allProducts(filter: { integerField: { neq: 3 } }) {
    title
  }
}
```

#### JSON fields

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { jsonField: { exists: true } }) {
    title
  }
}
```

#### Geolocation fields

`near`

Filter records within the specified radius in meters

```graphql
query {
  allProducts(
    filter: {
      latLonField: {
        near: { latitude: 40.73, longitude: -73.93, radius: 10 }
      }
    }
  ) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { latLonField: { exists: true } }) {
    title
  }
}
```

#### Single link fields

`eq`

Search for records with an exact match. The specified value must be a Record ID

```graphql
query {
  allProducts(filter: { linkField: { eq: "123" } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match. The specified value must be a Record ID

```graphql
query {
  allProducts(filter: { linkField: { neq: "123" } }) {
    title
  }
}
```

`in`

Filter records linked to one of the specified records

```graphql
query {
  allProducts(filter: { linkField: { in: ["123"] } }) {
    title
  }
}
```

`not_in`

Filter records not linked to one of the specified records

```graphql
query {
  allProducts(filter: { linkField: { notIn: ["123"] } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { linkField: { exists: true } }) {
    title
  }
}
```

#### Multiple links fields

`eq`

Search for records with an exact match. The specified values must be Record IDs

```graphql
query {
  allProducts(filter: { linksField: { eq: ["123"] } }) {
    title
  }
}
```

`all_in`

Filter records linked to all of the specified records. The specified values must be Record IDs

```graphql
query {
  allProducts(filter: { linksField: { allIn: ["123"] } }) {
    title
  }
}
```

`any_in`

Filter records linked to at least one of the specified records. The specified values must be Record IDs

```graphql
query {
  allProducts(filter: { linksField: { anyIn: ["123"] } }) {
    title
  }
}
```

`not_in`

Filter records not linked to any of the specified records. The specified values must be Record IDs

```graphql
query {
  allProducts(filter: { linksField: { notIn: ["123"] } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { linksField: { exists: true } }) {
    title
  }
}
```

#### SEO meta tags fields

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { seoField: { exists: true } }) {
    title
  }
}
```

#### Slug fields

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { slugField: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match

```graphql
query {
  allProducts(filter: { slugField: { neq: "bike" } }) {
    title
  }
}
```

`in`

Filter records that have one of the specified slugs

```graphql
query {
  allProducts(filter: { slugField: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Filter records that do have one of the specified slugs

```graphql
query {
  allProducts(filter: { slugField: { notIn: ["bike"] } }) {
    title
  }
}
```

#### Single-line string fields

`matches`

Filter records based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      stringField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude records based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      stringField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`is_blank`

Filter records with the specified field set as blank (null or empty string)

```graphql
query {
  allProducts(filter: { stringField: { isBlank: true } }) {
    title
  }
}
```

`is_present`

Filter records with the specified field present (neither null, nor empty string)

```graphql
query {
  allProducts(filter: { stringField: { isPresent: true } }) {
    title
  }
}
```

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { stringField: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match

```graphql
query {
  allProducts(filter: { stringField: { neq: "bike" } }) {
    title
  }
}
```

`in`

Filter records that equal one of the specified values

```graphql
query {
  allProducts(filter: { stringField: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Filter records that do not equal one of the specified values

```graphql
query {
  allProducts(filter: { stringField: { notIn: ["bike"] } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not \[DEPRECATED\]

```graphql
query {
  allProducts(filter: { stringField: { exists: true } }) {
    title
  }
}
```

#### Structured text fields

`matches`

Filter records based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      structuredTextField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude records based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      structuredTextField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`is_blank`

Filter records with the specified field set as blank (null or single empty paragraph)

```graphql
query {
  allProducts(filter: { structuredTextField: { isBlank: true } }) {
    title
  }
}
```

`is_present`

Filter records with the specified field present (neither null, nor empty string)

```graphql
query {
  allProducts(filter: { structuredTextField: { isPresent: true } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not \[DEPRECATED\]

```graphql
query {
  allProducts(filter: { structuredTextField: { exists: true } }) {
    title
  }
}
```

#### Multiple-paragraph text fields

`matches`

Filter records based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      textField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude records based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      textField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`is_blank`

Filter records with the specified field set as blank (null or empty string)

```graphql
query {
  allProducts(filter: { textField: { isBlank: true } }) {
    title
  }
}
```

`is_present`

Filter records with the specified field present (neither null, nor empty string)

```graphql
query {
  allProducts(filter: { textField: { isPresent: true } }) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not \[DEPRECATED\]

```graphql
query {
  allProducts(filter: { textField: { exists: true } }) {
    title
  }
}
```

#### Video fields

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { videoField: { exists: true } }) {
    title
  }
}
```

### Filters available for meta fields

#### Filter by `_createdAt` meta field

`gt`

Filter records with a value that's strictly greater than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter records with a value that's less than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter records with a value that's greater than or equal to than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter records with a value that's less or equal than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`eq`

Filter records with a value that's within the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Filter records with a value that's outside the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { _createdAt: { exists: true } }) {
    title
  }
}
```

#### Filter by `_firstPublishedAt` meta field

`gt`

Filter records with a value that's strictly greater than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _firstPublishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter records with a value that's less than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _firstPublishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter records with a value that's greater than or equal to than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _firstPublishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter records with a value that's less or equal than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _firstPublishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`eq`

Filter records with a value that's within the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _firstPublishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Filter records with a value that's outside the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _firstPublishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { _firstPublishedAt: { exists: true } }) {
    title
  }
}
```

#### Filter by `_isValid` meta field

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { _isValid: { eq: true } }) {
    title
  }
}
```

#### Filter by `_publicationScheduledAt` meta field

`gt`

Filter records with a value that's strictly greater than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publicationScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter records with a value that's less than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publicationScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter records with a value that's greater than or equal to than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publicationScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter records with a value that's less or equal than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publicationScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`eq`

Filter records with a value that's within the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publicationScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Filter records with a value that's outside the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publicationScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { _publicationScheduledAt: { exists: true } }) {
    title
  }
}
```

#### Filter by `_publishedAt` meta field

`gt`

Filter records with a value that's strictly greater than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter records with a value that's less than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter records with a value that's greater than or equal to than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter records with a value that's less or equal than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`eq`

Filter records with a value that's within the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Filter records with a value that's outside the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _publishedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { _publishedAt: { exists: true } }) {
    title
  }
}
```

#### Filter by `_status` meta field

`eq`

Search the record with the specified status

```graphql
query {
  allProducts(filter: { _status: { eq: draft } }) {
    title
  }
}
```

`neq`

Exclude the record with the specified status

```graphql
query {
  allProducts(filter: { _status: { neq: draft } }) {
    title
  }
}
```

`in`

Search records with the specified statuses

```graphql
query {
  allProducts(filter: { _status: { in: [draft] } }) {
    title
  }
}
```

`not_in`

Search records without the specified statuses

```graphql
query {
  allProducts(filter: { _status: { notIn: [draft] } }) {
    title
  }
}
```

#### Filter by `_unpublishingScheduledAt` meta field

`gt`

Filter records with a value that's strictly greater than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _unpublishingScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter records with a value that's less than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _unpublishingScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter records with a value that's greater than or equal to than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _unpublishingScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter records with a value that's less or equal than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _unpublishingScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`eq`

Filter records with a value that's within the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _unpublishingScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Filter records with a value that's outside the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _unpublishingScheduledAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { _unpublishingScheduledAt: { exists: true } }) {
    title
  }
}
```

#### Filter by `_updatedAt` meta field

`gt`

Filter records with a value that's strictly greater than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter records with a value that's less than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter records with a value that's greater than or equal to than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter records with a value that's less or equal than the one specified. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`eq`

Filter records with a value that's within the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Filter records with a value that's outside the specified minute range. Seconds and milliseconds are truncated from the argument.

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { _updatedAt: { exists: true } }) {
    title
  }
}
```

#### Filter by `id` meta field

`eq`

Search the record with the specified ID

```graphql
query {
  allProducts(filter: { id: { eq: "123" } }) {
    title
  }
}
```

`neq`

Exclude the record with the specified ID

```graphql
query {
  allProducts(filter: { id: { neq: "123" } }) {
    title
  }
}
```

`in`

Search records with the specified IDs

```graphql
query {
  allProducts(filter: { id: { in: ["123"] } }) {
    title
  }
}
```

`not_in`

Search records that do not have the specified IDs

```graphql
query {
  allProducts(filter: { id: { notIn: ["123"] } }) {
    title
  }
}
```

#### Filter by `parent` meta field

`eq`

Filter records children of the specified record. Value must be a Record ID

```graphql
query {
  allProducts(filter: { parent: { eq: "123" } }) {
    title
  }
}
```

`exists`

Filter records with a parent record or not

```graphql
query {
  allProducts(filter: { parent: { exists: true } }) {
    title
  }
}
```

#### Filter by `position` meta field

`gt`

Filter records with a value that's strictly greater than the one specified

```graphql
query {
  allProducts(filter: { position: { gt: 3 } }) {
    title
  }
}
```

`lt`

Filter records with a value that's less than the one specified

```graphql
query {
  allProducts(filter: { position: { lt: 3 } }) {
    title
  }
}
```

`gte`

Filter records with a value that's greater than or equal to the one specified

```graphql
query {
  allProducts(filter: { position: { gte: 3 } }) {
    title
  }
}
```

`lte`

Filter records with a value that's less or equal than the one specified

```graphql
query {
  allProducts(filter: { position: { lte: 3 } }) {
    title
  }
}
```

`eq`

Search for records with an exact match

```graphql
query {
  allProducts(filter: { position: { eq: 3 } }) {
    title
  }
}
```

`neq`

Exclude records with an exact match

```graphql
query {
  allProducts(filter: { position: { neq: 3 } }) {
    title
  }
}
```

---

# Content Delivery API — Deep Filtering

Source [docs]: https://www.datocms.com/docs/content-delivery-api/deep-filtering.md

[Modular Content](/docs/content-modelling/modular-content.md) and [Structured Text](/docs/content-modelling/structured-text.md) fields can embed [blocks](/docs/content-modelling/blocks.md), which are dynamic, flexible, and repeatable structures. When referring to the Content Delivery API, deep filtering allows you to **filter records based on the content within their embedded blocks**.

## Activate deep filtering

Deep filtering is a feature that **needs to be explicitly enabled on a per-field basis.** This is to avoid complicating — and therefore slowing down — your GraphQL schema by generating a large number of types that will never actually be used.

To activate the feature, go to a field's editing modal, and enable the deep filtering option:

(Image content)

Once enabled, new filters become available in the GraphQL Content Delivery API for the specified field.

To be precise, there is a second necessary condition to see the newly activated GraphQL filters: the field must accept at least one block type. If the field does not allow embedding any block types within it, then deep filtering is not applicable — *there's nothing to filter!* — and therefore the GraphQL filters relative to deep filtering will not be present.

> [!PROTIP] Pro tip: Explore use cases for deep filtering
> Check out this [blog entry](https://www.datocms.com/blog/advanced-data-retrieval-with-deep-filtering.md) for an in-depth look at use cases where deep filtering can simplify accessing specific data without multiple API calls, enhancing performance and efficiency.

## Modular Content

In the following examples, let's consider a `blog_post` model with a `content` Modular Content field that accepts two types of blocks: `hero` and `product`.

#### Filter records containing at least one block matching the specified conditions

To retrieve `blog_post` records that contain a `product` block with a `name` equal to `"T-Shirt"`, and a `price` greater than 30, you can use the following GraphQL query:

```graphql
query {
  allBlogPosts(
    filter: {
      content: {
        any: { product: { name: { eq: "T-Shirt" }, price: { gt: 30 } } }
      }
    }
  ) {
    # ...
  }
}
```

In other words, within a field with deep filtering enabled, you can specify an `any` key where, for each block type that the field accepts, you can define one or more filtering conditions. The word "any" can be read as: *"find records where any product block respects these conditions"*.

The filtering conditions are the same ones discussed in the previous section on [Filtering records](/docs/content-delivery-api/filtering-records.md), with the only difference being that since blocks don't have meta fields like ie. `_firstPublishedAt` they cannot be used in this context. The only meta key available for blocks is `id`.

#### Specifying conditions for multiple types of block

If you specify conditions for more than one type of block, then all conditions must be respected.

```graphql
query {
  allBlogPosts(
    filter: {
      content: {
        any: {
          product: { name: { eq: "T-Shirt" } }
          hero: { title: { matches: { pattern: "offer" } } }
        }
      }
    }
  ) {
    # ...
  }
}
```

The example above will search for all blog posts that have **both** a `product` block called `"T-Shirt"`, and a hero block with a title that contains the term `"offer"`.

#### Putting conditions in OR

If you want to apply logical OR conditions between various conditions, you can always use the `OR` filter.

The previous query can be modified to return blog posts that have either a "T-Shirt" `product` block or an "offer" `hero` block, like this:

```graphql
query {
  allBlogPosts(
    filter: {
      OR: [
        {
          content: {
            any: { product: { name: { eq: "T-Shirt" } } }
          }
        },
        {
          content: {
            any: { hero: { title: { matches: { pattern: "offer" } } } }
          }
        }
      ]
    }
  ) {
    # ...
  }
}
```

#### Filter records containing at least one block, of any kind

If you are only interested into filtering records that, in a particular field, contain at least one block, regardless of its type, you can use the `exists` filter:

```graphql
query {
  allBlogPosts(
    filter: {
      content: { exists: true }
    }
  ) {
    # ...
  }
}
```

Inverting the condition into `exists: false` will find all blog posts that don't have any block in the field.

#### Filter records containing at least one block of specified type

If you want to be more specific and filter records that contain at least one block of one or more specific types, then you can use the `"containsAny": true` filter. You can also search for records that do not contain any blocks of a specific type with`"containsAny": false`.

The following query returns all blog posts that contain at least one block of type `product`, but do not contain any blocks of type `hero`:

```graphql
query {
  allBlogPosts(
    filter: {
      content: {
        containsAny: { product: true, hero: false }
      }
    }
  ) {
    # ...
  }
}
```

## Structured Text

When deep filtering is *not* enabled on a Structured Text field, its GraphQL filters allow to filter by its textual content only, like this:

```graphql
query {
  allProducts(
    filter: {
      structuredTextField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    # ...
  }
}
```

However, when you activate deep filtering, the filter format will change, and the `value` argument will group together all filters related to the textual content.

To put it differently, when deep filtering is turned on, the previous query needs to be rewritten as:

```graphql
query {
  allProducts(
    filter: {
      structuredTextField: {
        value: { # this argument has been introduced
          matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
        }
      }
    }
  ) {
    # ...
  }
}
```

> [!WARNING] This means a breaking change in GraphQL schema!
> We just saw that enabling deep filtering on a Structured Text field will cause a change to the associated GraphQL filter type. This means that existing GraphQL queries may need to be rewritten in order to avoid errors.
> 
> This is another reason why deep filtering is activable or not on a per-field basis: so that you are in control of when (and how) to introduce this change.
> 
> To avoid unpleasant surprises in production, it is a good idea to test the switch to deep filtering for Structured Text fields in a a [sandbox environment](/docs/general-concepts/primary-and-sandbox-environments.md) first, and see if it breaks any of your existing GraphQL queries.

All the query possibilities in deep filtering mentioned above for the Modular Content fields also apply to the Structured Text fields.

The only distinction is that all the filter arguments related to blocks are nested inside the `blocks` argument:

```graphql
query {
  allProducts(
    filter: {
      structuredTextField: {
        blocks: {
          any: { cta: { title: { eq: "Subscribe!" } }
        },
        value: {
          matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
        }
      }
    }
  ) {
    # ...
    }
  }
}
```

## Known Issues

> [!NOTE] Deep Filtering Technical Limitations
> #### Depth Limit:
> 
> Deep filtering is currently limited to only one level of depth. That is, you cannot filter records based on the content of blocks deeply nested inside other blocks. For example, if you have:
> 
> -   A modular content field
>     
>     -   A parent block with a "parent title" field and another modular content field called "child blocks"
>         
>         -   Inside a child block, you have another field called "child title"
>             
> 
> You **can** filter by the "parent title" but you **cannot** filter by the "child title".
> 
> #### Content Delivery API only:
> 
> Deep filtering is currently limited to the Content Delivery API (CDA / GraphQL). You cannot deep filter records in the Content Management API (CMA / REST).

---

# Content Delivery API — Ordering records

Source [docs]: https://www.datocms.com/docs/content-delivery-api/ordering-records.md

## Ordering records

When retrieving records of a specific model you can supply the `orderBy` argument for every scalar field of the model: `orderBy: <field>_ASC` or `orderBy: <field>_DESC`. For tree and sortable models, you can also order them by `position`:

```graphql
query {
  allArtists(
    orderBy: [name_ASC]
  ) {
    id
    name
  }
}
```

---

# Content Delivery API — Pagination

Source [docs]: https://www.datocms.com/docs/content-delivery-api/pagination.md

Pagination lets you retrieve large sets of records by breaking them into manageable chunks — helping optimize performance and reduce data transfer.

### Limit results with `first`

Control the number of elements returned in a single query using the `first` parameter:

-   **Default limit:** 20 records
-   **Maximum limit:** 500 records
    

The following query returns the first 5 artist records:

```graphql
{
  allArtists(first: 5) {
    id
    name
  }
}
```

### Skip records with `skip`

Use the `skip` parameter to offset your results, allowing you to implement pagination across multiple requests:

```graphql
{
  allArtists(first: 5, skip: 10) {
    id
    name
  }
}
```

This query skips the first 10 records and returns the next 5.

### Calculating the total number of results

To determine the total number of records — typically to implement client-side pagination — use the `_XXXMeta` query:

```graphql
query {
  allArtists(filter: { name: { in: ["Blank Banshee", "Gazelle Twin"] } }) {
    id
    name
    genre
  }
  _allArtistsMeta(filter: { name: { in: ["Blank Banshee", "Gazelle Twin"] } }) {
    count
  }
}
```

Make sure to apply the same filters to both your regular query and the meta query, otherwise the two counts will diverge!

### Auto-pagination beyond the 500-record limit

A single CDA query can return at most 500 records per collection. For larger result sets, the [`@datocms/cda-client`](/docs/content-delivery-api/your-first-request.md) package ships an `executeQueryWithAutoPagination` helper that rewrites the query on the fly into multiple aliased selections, executes it in a single round-trip, and stitches the results back together — transparently:

```typescript
import { executeQueryWithAutoPagination } from '@datocms/cda-client';

const result = await executeQueryWithAutoPagination(
  `query BuildSitemapUrls {
    entries: allSuccessStories(first: 2500) {
      slug
    }
  }`,
  { token: process.env.DATOCMS_READONLY_TOKEN },
);
```

###### How it works

Suppose you want to execute the following query on a model with 2,500 records:

```graphql
query BuildSitemapUrls {
  allBlogPosts {
    slug
  }

  entries: allSuccessStories(first: 2500) {
    ...SuccessStoryUrlFragment
  }
}

fragment SuccessStoryUrlFragment on SuccessStoryRecord {
  slug
}
```

The CDA returns at most 500 items at a time. Normally you'd paginate manually by issuing the same query five times, each with an incremented `skip`. `executeQueryWithAutoPagination` does it for you in a single round-trip by rewriting the query like this:

```graphql
query BuildSitemapUrls {
  allBlogPosts {
    slug
  }
  splitted_0_entries: allSuccessStories(first: 500, skip: 0) {
    ...SuccessStoryUrlFragment
  }
  splitted_500_entries: allSuccessStories(first: 500, skip: 500) {
    ...SuccessStoryUrlFragment
  }
  splitted_1000_entries: allSuccessStories(first: 500, skip: 1000) {
    ...SuccessStoryUrlFragment
  }
  splitted_1500_entries: allSuccessStories(first: 500, skip: 1500) {
    ...SuccessStoryUrlFragment
  }
  splitted_2000_entries: allSuccessStories(first: 500, skip: 2000) {
    ...SuccessStoryUrlFragment
  }
}

fragment SuccessStoryUrlFragment on SuccessStoryRecord {
  slug
}
```

Once executed, the results are collected and recomposed as if nothing happened.

###### Limitations

-   The query may contain **at most one** selection with an oversized `first:` argument. If two or more collections exceed 500 records in the same query, the helper throws.
-   The rewritten query must still respect the [GraphQL complexity cost limit](/docs/content-delivery-api/complexity.md).
    

###### Reading cache tags alongside auto-pagination

A `rawExecuteQueryWithAutoPagination` variant is also available, with the same `[result, response]` return shape as [`rawExecuteQuery`](/docs/content-delivery-api/api-endpoints.md). It's useful when you need to combine auto-pagination with [Cache Tags](/docs/content-delivery-api/cache-tags.md):

```typescript
import { rawExecuteQueryWithAutoPagination } from '@datocms/cda-client';

const [result, response] = await rawExecuteQueryWithAutoPagination(query, {
  token: process.env.DATOCMS_READONLY_TOKEN,
  returnCacheTags: true,
});

const cacheTags = response.headers.get('x-cache-tags');
```

You can find the complete API reference in the [`@datocms/cda-client` README](https://github.com/datocms/cda-client#executequerywithautopagination).

---

# Content Delivery API — Localization

Source [docs]: https://www.datocms.com/docs/content-delivery-api/localization.md

### Get your project locales

First, you can fetch the list of locales configured in a project with the following query:

```graphql
query {
  _site {
    locales # -> ["en", "it", "fr"]
  }
}
```

### Get the localizations available for a record

In case you have a model with some localized fields, and the model itself does not require the presence of a localization for each one of the locales configured in a project, you might need to know which localizations are actually present for each record.

The `_locales` field gives you exactly this information:

```graphql
query {
  allBlogPosts {
    _locales # -> ["en", "it"]
    title
  }
}
```

### Filter records by available localizations

The same `_locales` field can also be used to filter your records. For example, you can fetch only the records which have both an `en` and `it` localizations this way:

```graphql
query {
  allBlogPosts(filter: {_locales: {allIn: [it]}}) {
    title
  }
}
```

You can also use the `anyIn` criteria to fetch records which contain at least one of the requested localizations, or `notIn` to fetch records which do not have any of the specified localizations.

### Fetching localized content

When you're fetching the value of a [localized field](/docs/general-concepts/localization.md), by default it will be returned in the project default locale — that is, the first locale in your project settings:

```graphql
query {
  _site {
   locales # -> ["en", "it"]
 }
  allBlogPosts {
    title  # -> will return the title value in "en" locale
  }
}
```

To change that, you can add a `locale` argument to queries to specify another locale:

```graphql
query {
  allBlogPosts(locale: it) {
    title  # -> will return the title value in "it" locale
  }
}
```

You can also specify a different locale on a per-field basis:

```graphql
query {
  allBlogPosts(locale: it) {
    title # -> will return the title value in "it" locale
    enTitle: title(locale: en) # -> will return the title value in "en" locale
  }
}
```

### Fallback locales

You can also specify a list of fallback locales (see the "Fallback Locales" section of [Localization](/docs/general-concepts/localization.md) ) together with the `locale` argument:

```graphql
query {
  allBlogPosts(locale: it_IT, fallbackLocales: [it, en]) {
    title
  }
}
```

If the field value for the specified `locale` is `null`\-ish (`null`, empty string or empty array), the system will try to find a non `null`\-ish value in each of the localizations specified in the `fallbackLocales` argument. The order of the elements in the `fallbackLocales` argument is important, as the system will start from the first element in the array, and go on from there.

Just like the `locale` argument, you can specify different fallback locales on a per-field basis:

```graphql
query {
  allBlogPosts {
    title(locale: it_IT, fallbackLocales: [it, en])
  }
}
```

### Fetching all localizations

If you want to get the value of a field in every available localization, you can use the `_all[FIELD]Locales` field:

```graphql
query {
  allBlogPosts {
    _allTitleLocales {
      locale
      value
    } # -> returns [{ locale: "en", value: "Hi!"}, { locale: "it", value: "Ciao!"}]
  }
}
```

#### Learn more about localization with DatoCMS

DatoCMS allows a great deal of customization when dealing with localization. Check out these tutorial videos for a hands-on approach:

[

(Image content)

Localizing Content in DatoCMS

Play video »

](https://youtu.be/166gt1Qg-d4)

[

(Image content)

Creating a localized blog using Next.js

Play video »

](https://youtu.be/3tBeOwdVuwo)

---

# Content Delivery API — Direct vs. Inverse relationships

Source [docs]: https://www.datocms.com/docs/content-delivery-api/inverse-relationships.md

[Link fields](/docs/content-modelling/links.md) allow you to define relationships between records — e.g., a "blog post" record can reference a "person" record through an "author" link field.

Using the Content Delivery API, it is possible to follow such links between records in both directions. Continuing with our example:

-   Starting from a blog post, get its author (that's the *direct relationship* expressed by the link field in the blog post record).
-   Starting from a person, get all their blog posts (that's the *inverse relationship*, automatically derived by looking at the value of the author field in every blog post).
    

## Following a direct relationship

It is trivial to fetch information about a record that's directly referenced through a link field: just use the ID of the field [like you would with any other](/docs/content-delivery-api/how-to-fetch-records.md):

```graphql
query {
  allBlogPosts {
    title
    author {
      id
      firstName
      lastName
    }
  }
}
```

## Following an inverse relationship

If you're interested in the collection of records that are referencing a specific record of your interest, first you need to enable the "Enable inverse relationships fields in GraphQL?" option in the model settings — in our example, the "person" model:

(Video content)

Once the option is enabled, you will be able to perform inverse relationship queries using the `_allReferencingXXX` GraphQL field:

```graphql
query {
  allPeople {
    id
    name
    _allReferencingBlogPosts {
      id
      title
    }
  }
}
```

> [!POSITIVE] Inverse relationship queries are blocks-aware!
> Inverse relationship queries will also return results for links present inside some [blocks](/docs/content-modelling/blocks.md) embedded in the record, no matter the depth.

#### Pagination

With no arguments, the result will be the first 20 referencing records, but just like with regular collection queries, you can [paginate your results](/docs/content-delivery-api/pagination.md), and get the total number of records with the `_allReferencingXXXMeta` field:

```graphql
query {
  allPeople {
    _allReferencingBlogPosts(first: 5, skip: 10) {
      ...
    }
    _allReferencingBlogPostsMeta {
      count
    }
  }
}
```

#### Filtering references by field

Suppose that our blog post model has two different link fields that point to the same "person" model: the author and the reviewer.

If you're interested in only getting blog posts that link to a person via a specific field (e.g., the reviewer field), you can use the `through: { fields: }` argument:

```graphql
query {
  allPeople {
    _allReferencingBlogPosts(through: {fields: {anyIn: [blogPost_reviewer]}}) {
      id
      title
    }
  }
}
```

With the `through: { fields: }` argument is also possible to get only references coming from fields defined inside the record blocks embedded in a record via [Modular Content](/docs/content-modelling/modular-content.md) or [Structured Text](/docs/content-modelling/structured-text.md) fields.

As an example, the following inverse relationship query:

```graphql
query {
  allPeople {
    _allReferencingDocPages(
      through: {fields: {anyIn: [docPage_main__chapter_author]}}
    ) { ... }
  }
}
```

Will only return documentation pages which link to a specific author through the `author` link of the blocks of type `chapter` defined inside the `main` field of the page itself.

> [!WARNING] Use the API Explorer to make it easier to write your queries!
> As you can see from the last example, arguments like `docPage_main__chapter_author` can be tricky to write, and the situation can get much worse when you start considering nested blocks!
> 
> Always remember that in GraphQL you can harness the powers of introspection and use the API Explorer in your project to get query intelligent code-completion.

#### Filtering references by locale

Suppose that the "author" link in our blog post model is localized, so depending on the locale, the author of the blog post will be a different record.

To filter references in a specific set of locales, ignoring the others, you can use the `through: { locales: }` argument:

```graphql
query {
  allPeople {
    _allReferencingBlogPosts(through: {locales: {anyIn: [en]}}) {
      id
      title
    }
  }
}
```

Likewise, if you need to filter references that are coming from non-localized fields, you can use the `_nonLocalized` enum value:

```graphql
query {
  allPeople {
    _allReferencingBlogPosts(through: {locales: {anyIn: [_nonLocalized]}}) {
      id
      title
    }
  }
}
```

#### Ordering

To retrieve references in a specific order, you can use the `orderBy` argument. Suppose the "blog post" model has a `title` string field, you can specify the order like this:

```graphql
query {
  allPeople {
    _allReferencingBlogPosts(orderBy: title_ASC) {
      id
      title
    }
  }
}
```

#### Deep filtering

You can even filter references based on one or more of its fields:

```graphql
query {
  allPeople {
    _allReferencingBlogPosts(filter: {name: {matches: {pattern: "trip"}}}) {
      id
      title
    }
  }
}
```

---

# Content Delivery API — Hierarchical sorting (Tree-like collections)

Source [docs]: https://www.datocms.com/docs/content-delivery-api/hierarchical-sorting.md

If you have models using [hierarchical sorting](/docs/content-modelling/hierarchical-sorting.md), you can use the `children` and `parent` attributes to find the top-level objects of the model and then navigate in depth:

```graphql
query {
  allCategories(filter: {parent: {exists: false}}) {
    name
    children {
      name
      children {
        name
        children {
          name
        }
      }
    }
  }
}
```

---

# Content Delivery API — Modular content fields

Source [docs]: https://www.datocms.com/docs/content-delivery-api/modular-content-fields.md

If you have [Modular Content fields](/docs/content-modelling/modular-content.md), you can use GraphQL fragments to fetch information about all their embedded blocks.

Suppose a `blog_post` model has a modular content field called `content`, which in turn accepts the following [block models](/docs/content-modelling/modular-content.md):

-   Block `blog_post_text_block`: made of a `text` field (*multi-paragraph text*);
-   Block `blog_post_quote_block`: made of a `quote` field (*multi-paragraph text*) and `author` field (*single-line string*);
    
-   Block `blog_post_gallery_block`: made of a `gallery` field (*image gallery*);
    

This GraphQL query will do the work:

```graphql
query {
  allBlogPosts {
    title
    content {
      ... on BlogPostTextBlockRecord {
        id
        _modelApiKey
        text
      }
      ... on BlogPostQuoteBlockRecord {
        id
        _modelApiKey
        quote
        author
      }
      ... on BlogPostGalleryBlockRecord {
        id
        _modelApiKey
        gallery { url }
      }
    }
  }
}
```

Since all records implement the GraphQL interface `RecordInterface`, you can dry up the same query like this:

```graphql
query {
  allBlogPosts {
    title
    content {
      ... on RecordInterface {
        id
        _modelApiKey
      }
      ... on BlogPostTextBlockRecord {
        text
      }
      ... on BlogPostQuoteBlockRecord {
        quote
        author
      }
      ... on BlogPostGalleryBlockRecord {
        gallery { url }
      }
    }
  }
}
```

The outcome of this query hinges on the type of Modular Content field. If we're dealing with the Multiple Blocks variant, it'll return an array of blocks. However, if we're working with the Single Block variant, it'll simply return one block, or `null` if it's absent.

### Filtering records by contained blocks

If you need to filter records based on the content within their embedded blocks, please refer to the [Deep filtering](/docs/content-delivery-api/deep-filtering.md) section of this guide, where this scenario is explained in detail.

---

# Content Delivery API — Structured text fields

Source [docs]: https://www.datocms.com/docs/content-delivery-api/structured-text-fields.md

If you have [Structured Text fields](/docs/content-modelling/structured-text.md) you can use GraphQL fragments to fetch the different blocks.

Suppose a `blog_post` model has a Structured Text field called `content`, which in turn accepts [links](/docs/content-modelling/structured-text.md#linking-records) to other blog posts and the following [embedded blocks](/docs/content-modelling/structured-text.md#embedding-blocks):

-   Block `cta_block`: with a `label` and `url` fields (both *Single-line text*)
-   Block `carousel_block`: with an *Asset Gallery* field called `gallery`
    
-   Block `mention_block`: with a Single-line text field called `username`
    

This GraphQL query will return all the data needed to render it:

```graphql
query {
  allBlogPosts {
    title
    content {
      value
      blocks {
        __typename
        ... on RecordInterface {
          id
        }
        ... on CtaBlockRecord {
          label
          url
        }
        ... on CarouselBlockRecord {
          gallery { url }
        }
      }
      inlineBlocks {
        __typename
        ... on RecordInterface {
          id
        }
        ... on MentionBlockRecord {
          username
        }
      }
      links {
        __typename
        ... on RecordInterface {
          id
        }
        ... on BlogPostRecord {
          slug
          title
        }
      }
    }
  }
}
```

### Rendering Structured Text content

You can then use the result of this query with one of the following libraries to render the result as HTML:

-   [`datocms-structured-text-to-plain-text`](https://github.com/datocms/structured-text/tree/main/packages/to-plain-text) to render it as plain text;
-   [`datocms-structured-text-to-html-string`](https://github.com/datocms/structured-text/tree/main/packages/to-html-string) to render it as an HTML string;
    
-   [`datocms-structured-text-to-dom-nodes`](https://github.com/datocms/structured-text/tree/main/packages/to-dom-nodes) to transform it in a list of DOM nodes;
    

We also have ready-made components for the most popular frontend frameworks:

-   [React](https://github.com/datocms/react-datocms#structured-text)
-   [Vue](https://github.com/datocms/vue-datocms#structured-text)
    
-   [Svelte](https://github.com/datocms/datocms-svelte/tree/main/src/lib/components/StructuredText)
-   [Astro](https://github.com/datocms/astro-datocms/tree/main/src/StructuredText)
    

### Filtering records by contained blocks

If you need to filter records based on the content within their embedded blocks, please refer to the [Deep filtering](/docs/content-delivery-api/deep-filtering.md) section of this guide, where this scenario is explained in detail.

---

# Content Delivery API — Images and videos

Source [docs]: https://www.datocms.com/docs/content-delivery-api/images-and-videos.md

All the assets are augmented with some extra fields exposed via the GraphQL API, providing you some extra possibilities on the frontend.

### Images

Besides all the fields that you can explore via the CMS interface, the API can return both the [BlurHash](https://blurha.sh/) and the [ThumbHash](https://evanw.github.io/thumbhash/) of every image, also as a [Data-URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs).

You can embed the Data URL directly in the HTML of the page and then swap it with the actual image at a later time, to offer a smooth experience when loading images (LQIP).

If you're on [React](/docs/next-js/managing-images.md), [Vue](/docs/nuxt.md), or [Svelte](/docs/svelte/managing-images.md) our Image components make everything extremely simple to implement.

Alternatively, a more minimal option is to use the dominant colors to prepare the space where the image will be shown:

```graphql
{
  allUploads {
    blurhash
    thumbhash
    blurUpThumb
    colors { hex }
  }
}
```

#### Responsive images

One special augmentation that we offer on top of images in our GraphQL API is the `responsiveImage` object.

In this object you can find pre-computed image attributes that will help you setting up responsive images in your frontend without any additional manipulation.

We support all the [imgix parameters](https://docs.imgix.com/apis/url) and also, for extra control, the [sizes](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes) argument, that we simply return inside the response so that you can control media query conditions:

```graphql
{
  allUploads {
    responsiveImage(imgixParams: {fm: jpg, fit: crop, w: 600, h: 600}, sizes: "(max-width: 600px) 100vw, 600px") {
      # always required
      src
      srcSet
      width
      height

      # not required, but strongly suggested!
      alt
      title

      # LQIP (base64-encoded)
      base64
      # Alternatively, a background color placeholder
      bgColor

      # you can omit 'sizes' if you explicitly pass the 'sizes' prop to the image component
      sizes
    }
  }
```

One particularly handy feature of the CDA Playground in DatoCMS is that you can explore all the imgix parameters and read the documentation by searching for them in the docs panel:

(Video content)

For imgix Parameters that accept more than one value, you can pass them as an array in your graphQL query manually:

(Image content)

To read all the details of the `responsiveImage` object head to [the blog post](https://www.datocms.com/blog/best-way-for-handling-react-images.md#putting-it-all-together-introducing-the-responsiveimage-query) where you can also find some examples and integrations.

#### Focal points

Every image in the CDA exposes a `focalPoint` field with the coordinates set by editors in the media area:

```graphql
{
  allUploads {
    focalPoint {
      x  # float from 0.0 (left) to 1.0 (right)
      y  # float from 0.0 (top) to 1.0 (bottom)
    }
  }
}
```

More importantly, the focal point is **applied automatically** to `responsiveImage` and `url` responses. When all of the following conditions are met, DatoCMS transparently injects [`fp-x`](https://docs.imgix.com/apis/rendering/focal-point-crop/focal-point-x-position), [`fp-y`](https://docs.imgix.com/apis/rendering/focal-point-crop/focal-point-y-position), and [`crop=focalpoint`](https://docs.imgix.com/apis/rendering/size/crop-mode) into the imgix parameters, with no changes needed on your side:

-   [`fit: crop`](https://docs.imgix.com/apis/rendering/size/resize-fit-mode) is set in `imgixParams`
-   A size is defined: either [`w`](https://docs.imgix.com/apis/rendering/size/image-width) + [`h`](https://docs.imgix.com/apis/rendering/size/image-height) together, or [`ar`](https://docs.imgix.com/apis/rendering/size/aspect-ratio)
    
-   No conflicting [`crop`](https://docs.imgix.com/apis/rendering/size/crop-mode) mode is set (modes like `entropy` or `faces` take priority and suppress the injection)
-   You have not manually specified `fp-x` or `fp-y` in the query (if you have, your values are used as-is)
    
-   The focal point is not at the default center (0.5, 0.5), which would have no effect on the crop
    

For example, this query:

```graphql
{
  allUploads {
    responsiveImage(imgixParams: {fit: crop, w: 400, h: 400}) {
      src
      srcSet
    }
  }
}
```

...automatically produces URLs with `crop=focalpoint&fp-x=...&fp-y=...` for any image that has a non-centered focal point set.

During development, adding [`fp-debug: true`](https://docs.imgix.com/apis/rendering/focal-point-crop/focal-point-debug) to `imgixParams` renders a visual overlay that shows exactly where the focal point lands on the image.

To learn how to set the focal point on an image from the media area, see [Images API](/docs/asset-api/images.md) .

### Videos

> [!WARNING] Use HLS streaming whenever possible
> In order to save costs and improve visitor UX, we strongly recommend that you serve videos via HLS (HTTP Live Streaming) whenever possible, instead of using the raw MP4 videos.
> 
> HLS is easily served with our video components (below). Please see [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md) for a more detailed explanation.

If you chose to upload videos on DatoCMS, thanks to the integration with [Mux](https://www.mux.com/), we augment the CDA `video` objects with:

-   The Mux Playback ID, required for our `<VideoPlayer/>` component, or if you're using one of [Mux's players for other platforms](https://www.mux.com/docs/guides/play-your-videos)
-   HLS video streaming URL — we offer `<VideoPlayer />` components for [React](https://github.com/datocms/react-datocms/blob/master/docs/video-player.md), [Vue](https://github.com/datocms/vue-datocms/tree/master/src/components/VideoPlayer) and [Svelte,](https://github.com/datocms/datocms-svelte/tree/main/src/lib/components/VideoPlayer) which act as a wrapper around [Mux's video player](https://github.com/muxinc/elements/blob/main/packages/mux-player/README.md) web component. Alternatively, you can learn [how to integrate the Mux video player into your frontend](https://docs.mux.com/guides/player/integrate-in-your-webapp);
    
-   High, medium and low quality MP4 versions of the video to support legacy browsers that do not support HLS;
-   Duration and frame rate of the video;
    
-   Thumbnail URL: resizable, croppable and available in JPEG, PNG and GIF format. See [Mux thumbnail query string parameters](https://docs.mux.com/guides/get-images-from-a-video#thumbnail-query-string-parameters) for available transformations.
    

#### Example CDA Video Query

This is an example GraphQL query on a `video` field:

```graphql
{
  yourField { # API name of your media field
    id # The internal DatoCMS ID of the video, useful for navigating to it in the media area
    video { # The actual Mux video object
      muxPlaybackId # Playback ID, REQUIRED for the <VideoPlayer/> component
      streamingUrl # HLS URL for third-party players, e.g. https://stream.mux.com/{playbackId}.m3u8
      mp4High: mp4Url(res: high) # Raw MP4 URL in high quality, https://stream.mux.com/{playbackId}/high.mp4
      mp4Med: mp4Url(res: medium) # or medium.mp4
      mp4Low: mp4Url(res: low) # low.mp4
      width # In pixels, e.g. 1920
      height # 1080
      duration # seconds
      framerate # frames per second
      thumbJpg: thumbnailUrl(format: jpg) # https://image.mux.com/{playbackId}/thumbnail.jpg
      thumbPng: thumbnailUrl(format: png) # or thumbnail.png
      thumbGif: thumbnailUrl(format: gif) # thumbnail.gif
      thumbhash: # base64 string that encodes a thumbhash preview image
      posterTime # seconds with decimals, e.g. 2.345
    }
  }
}
```

#### Poster time

Every video in the CDA exposes a `posterTime` field with the timestamp (in seconds, with decimals) that editors selected in the media area or in the record field as the representative frame of the video:

```graphql
{
  yourField {
    video { # The actual Mux video object
      posterTime # seconds with decimals, e.g. 2.345
    }
  }
}
```

More importantly, the poster time is **applied automatically** to the `thumbnailUrl` response.

When an editor has set a poster time, DatoCMS transparently injects the [time](https://docs.mux.com/guides/get-images-from-a-video#thumbnail-query-string-parameters) parameter into the generated Mux thumbnail URL, with no changes needed on your side.

For example, the `thumbnailUrl` in this query:

```graphql
{
  yourField {
    video {
      thumbnailUrl(format: jpg)
    }
  }
}
```

...produces a URL like `https://image.mux.com/{playbackId}/thumbnail.jpg?``**time=2.345**` for a video that has the poster time set at 2.345 seconds. This means the still image you get from `thumbnailUrl` — and the poster frame shown by our `<VideoPlayer />` components before playback begins — matches the frame the editor deliberately chose, rather than defaulting to the start of the clip.

### Filtering

You can filter on all the meaningful fields that we offer in the uploads.

Here's an example of what you'll see in your CDA Playground:

(Image content)

### Fetch uploads straight from the context

For the GraphQL veterans this will be obvious, but still we are impressed how cool it is to be able to fetch all the augmented assets directly from the context where they are used:

```graphql
{
  allAuthors {
    name
    avatar {
      responsiveImage {
        base64
        sizes
        srcSet
        alt
        title
      }
    }
  }
}
```

### Image width & height errors with certain Imgix operations

In most cases, our API will return the correct `width` and `height` for your images, which is necessary for correct rendering on the frontend.

However, in some edge cases, like when using the Imgix [`trim`](https://docs.imgix.com/en-US/apis/rendering/trim) operation (and also [padding](https://docs.imgix.com/en-US/apis/rendering/border-and-padding/padding) and [rotation](https://docs.imgix.com/en-US/apis/rendering/rotation)), our API cannot know the true dimensions of the transformed image beforehand. This means that **using those transformations will cause our API to return the incorrect image width and height**, and you must manually calculate and override them on your frontend instead, like:

```tsx
{/* Destructure the original responsiveImage object, then override its dimensions */}
<Image data={{...myQueryResponse.responsiveImage, width: 200, height: 200}} />
```

Alternatively, you can download the transformed image (e.g. `https://www.datocms-assets.com/12345/example.png?trim=color`) and re-upload that transformation back into your DatoCMS media area as a separate file and use that directly.

If you need any help with this, please contact our support team at [support@datocms.com](mailto:support@datocms.com).

---

# Content Delivery API — Filtering uploads

Source [docs]: https://www.datocms.com/docs/content-delivery-api/filtering-uploads.md

You can supply different parameters to the `filter` argument to filter the query response accordingly:

```graphql
query {
  allUploads(filter: { type: { eq: image } }) {
    url
    copyright
    exifInfo
  }
}
```

If you specify multiple conditions, they will be combined as if it was a logical `AND` expression:

```graphql
query {
  allUploads(filter: { type: { eq: image }, resolution: { eq: large }}) {
    blurUpThumb
    url(imgixParams: { w: 100, h: 100, fit: crop })
  }
}
```

You can also combine `AND` and `OR` logical expressions. For example, the following query will return all large images together with any video tagged with "fun":

```graphql
query {
  allUploads(
    filter: {
      OR: [
        { type: { eq: image }, resolution: { eq: large }},
        { type: { eq: video }, tags: { contains: "fun" }}
      ]
    }
  ) {
    blurUpThumb
    url(imgixParams: {w: 100, h: 100, fit: crop})
  }
}
```

## Available filters

#### Filter by `_createdAt`

`eq`

Search for uploads with an exact match

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Exclude uploads with an exact match

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter uploads with a value that's less than the one specified

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter uploads with a value that's less or equal than the one specified

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gt`

Filter uploads with a value that's strictly greater than the one specified

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter uploads with a value that's greater than or equal to the one specified

```graphql
query {
  allProducts(
    filter: {
      _createdAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

#### Filter by `_updatedAt`

`eq`

Search for uploads with an exact match

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        eq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`neq`

Exclude uploads with an exact match

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        neq: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lt`

Filter uploads with a value that's less than the one specified

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`lte`

Filter uploads with a value that's less or equal than the one specified

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        lte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gt`

Filter uploads with a value that's strictly greater than the one specified

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gt: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

`gte`

Filter uploads with a value that's greater than or equal to the one specified

```graphql
query {
  allProducts(
    filter: {
      _updatedAt: {
        # Value is truncated to the minute: "2018-02-13T14:30:00+00:00"
        gte: "2018-02-13T14:30:13+00:00"
      }
    }
  ) {
    title
  }
}
```

> [!NOTE] Filtering adjacent records
> Truncation to the nearest minute may cause filters to return unintended records (e.g., `gt: "2025-05-06T09:36:01+02:00"` becomes `gt: "2025-05-06T09:36:00+02:00"` and unexpectedly includes records at `2025-05-06T09:36:01+02:00`).
> 
> Add an additional filter condition (like `slug: {neq: $slug}` in the example below) to ensure unintended records are excluded from the results:
> 
> ```graphql
> query NextArticle($slug: String, $firstPublishedAt: DateTime) {
>   next: article(
>     orderBy: _firstPublishedAt_ASC
>     filter: {
>       _firstPublishedAt: {gt: $firstPublishedAt},
>       slug: {neq: $slug}
>     }
>   ) {
>     title
>     _firstPublishedAt
>   }
> }
> ```

#### Filter by `alt`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      altField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      altField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`eq`

Search the uploads with the specified alt

```graphql
query {
  allProducts(filter: { altField: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude the uploads with the specified alt

```graphql
query {
  allProducts(filter: { altField: { neq: "bike" } }) {
    title
  }
}
```

`in`

Search uploads with the specified values as default alt

```graphql
query {
  allProducts(filter: { altField: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Search uploads that do not have the specified values as default alt

```graphql
query {
  allProducts(filter: { altField: { notIn: ["bike"] } }) {
    title
  }
}
```

`exists`

Filter uploads with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { altField: { exists: true } }) {
    title
  }
}
```

#### Filter by `author`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      authorField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      authorField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`exists`

Filter uploads with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { authorField: { exists: true } }) {
    title
  }
}
```

#### Filter by `basename`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      basenameField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      basenameField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

#### Filter by `colors`

`contains`

Filter uploads that have the specified colors

```graphql
query {
  allProducts(filter: { colorsField: { contains: red } }) {
    title
  }
}
```

`all_in`

Filter uploads that have all of the specified colors

```graphql
query {
  allProducts(filter: { colorsField: { allIn: [red] } }) {
    title
  }
}
```

`any_in`

Filter uploads that have at least one of the specified colors

```graphql
query {
  allProducts(filter: { colorsField: { anyIn: [red] } }) {
    title
  }
}
```

`not_in`

Filter uploads that do not have any of the specified colors

```graphql
query {
  allProducts(filter: { colorsField: { notIn: [red] } }) {
    title
  }
}
```

`eq`

Search for uploads with an exact match

```graphql
query {
  allProducts(filter: { colorsField: { eq: [red] } }) {
    title
  }
}
```

#### Filter by `copyright`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      copyrightField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      copyrightField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { copyrightField: { exists: true } }) {
    title
  }
}
```

#### Filter by `filename`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      filenameField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      filenameField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

#### Filter by `format`

`eq`

Search the asset with the specified format

```graphql
query {
  allProducts(filter: { formatField: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude the asset with the specified format

```graphql
query {
  allProducts(filter: { formatField: { neq: "bike" } }) {
    title
  }
}
```

`in`

Search assets with the specified formats

```graphql
query {
  allProducts(filter: { formatField: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Search assets that do not have the specified formats

```graphql
query {
  allProducts(filter: { formatField: { notIn: ["bike"] } }) {
    title
  }
}
```

#### Filter by `height`

`gt`

Search all assets larger than the specified height

```graphql
query {
  allProducts(filter: { heightField: { gt: 3 } }) {
    title
  }
}
```

`lt`

Search all assets smaller than the specified height

```graphql
query {
  allProducts(filter: { heightField: { lt: 3 } }) {
    title
  }
}
```

`gte`

Search all assets larger or equal to the specified height

```graphql
query {
  allProducts(filter: { heightField: { gte: 3 } }) {
    title
  }
}
```

`lte`

Search all assets larger or equal to the specified height

```graphql
query {
  allProducts(filter: { heightField: { lte: 3 } }) {
    title
  }
}
```

`eq`

Search assets with the specified height

```graphql
query {
  allProducts(filter: { heightField: { eq: 3 } }) {
    title
  }
}
```

`neq`

Search assets that do not have the specified height

```graphql
query {
  allProducts(filter: { heightField: { neq: 3 } }) {
    title
  }
}
```

#### Filter by `id`

`eq`

Search the asset with the specified ID

```graphql
query {
  allProducts(filter: { id: { eq: "123" } }) {
    title
  }
}
```

`neq`

Exclude the asset with the specified ID

```graphql
query {
  allProducts(filter: { id: { neq: "123" } }) {
    title
  }
}
```

`in`

Search assets with the specified IDs

```graphql
query {
  allProducts(filter: { id: { in: ["123"] } }) {
    title
  }
}
```

`not_in`

Search assets that do not have the specified IDs

```graphql
query {
  allProducts(filter: { id: { notIn: ["123"] } }) {
    title
  }
}
```

#### Filter by `inUse`

`eq`

Search uploads that are currently used by some record or not

```graphql
query {
  allProducts(filter: { inUseField: { eq: true } }) {
    title
  }
}
```

#### Filter by `md5`

`eq`

Search the asset with the specified MD5

```graphql
query {
  allProducts(filter: { md5Field: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude the asset with the specified MD5

```graphql
query {
  allProducts(filter: { md5Field: { neq: "bike" } }) {
    title
  }
}
```

`in`

Search assets with the specified MD5s

```graphql
query {
  allProducts(filter: { md5Field: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Search assets that do not have the specified MD5s

```graphql
query {
  allProducts(filter: { md5Field: { notIn: ["bike"] } }) {
    title
  }
}
```

#### Filter by `mimeType`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      mimeTypeField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      mimeTypeField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`eq`

Search the asset with the specified mime type

```graphql
query {
  allProducts(filter: { mimeTypeField: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude the asset with the specified mime type

```graphql
query {
  allProducts(filter: { mimeTypeField: { neq: "bike" } }) {
    title
  }
}
```

`in`

Search assets with the specified mime types

```graphql
query {
  allProducts(filter: { mimeTypeField: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Search assets that do not have the specified mime types

```graphql
query {
  allProducts(filter: { mimeTypeField: { notIn: ["bike"] } }) {
    title
  }
}
```

#### Filter by `notes`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      notesField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      notesField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`exists`

Filter records with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { notesField: { exists: true } }) {
    title
  }
}
```

#### Filter by `orientation`

`eq`

Search uploads with the specified orientation

```graphql
query {
  allProducts(filter: { orientationField: { eq: landscape } }) {
    title
  }
}
```

`neq`

Exclude uploads with the specified orientation

```graphql
query {
  allProducts(filter: { orientationField: { neq: landscape } }) {
    title
  }
}
```

#### Filter by `path`

`eq`

Search the asset with the specified path

```graphql
query {
  allProducts(filter: { pathField: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude the asset with the specified path

```graphql
query {
  allProducts(filter: { pathField: { neq: "bike" } }) {
    title
  }
}
```

`in`

Search assets with the specified paths

```graphql
query {
  allProducts(filter: { pathField: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Search assets that do not have the specified paths

```graphql
query {
  allProducts(filter: { pathField: { notIn: ["bike"] } }) {
    title
  }
}
```

#### Filter by `resolution`

`eq`

Search uploads with the specified resolution

```graphql
query {
  allProducts(filter: { resolutionField: { eq: icon } }) {
    title
  }
}
```

`neq`

Exclude uploads with the specified resolution

```graphql
query {
  allProducts(filter: { resolutionField: { neq: icon } }) {
    title
  }
}
```

`in`

Search uploads with the specified resolutions

```graphql
query {
  allProducts(filter: { resolutionField: { in: [icon] } }) {
    title
  }
}
```

`not_in`

Search uploads without the specified resolutions

```graphql
query {
  allProducts(filter: { resolutionField: { notIn: [icon] } }) {
    title
  }
}
```

#### Filter by `size`

`gt`

Search all assets larger than the specified size (in bytes)

```graphql
query {
  allProducts(filter: { sizeField: { gt: 3 } }) {
    title
  }
}
```

`lt`

Search all assets smaller than the specified size (in bytes)

```graphql
query {
  allProducts(filter: { sizeField: { lt: 3 } }) {
    title
  }
}
```

`gte`

Search all assets larger or equal to the specified size (in bytes)

```graphql
query {
  allProducts(filter: { sizeField: { gte: 3 } }) {
    title
  }
}
```

`lte`

Search all assets larger or equal to the specified size (in bytes)

```graphql
query {
  allProducts(filter: { sizeField: { lte: 3 } }) {
    title
  }
}
```

`eq`

Search assets with the specified size (in bytes)

```graphql
query {
  allProducts(filter: { sizeField: { eq: 3 } }) {
    title
  }
}
```

`neq`

Search assets that do not have the specified size (in bytes)

```graphql
query {
  allProducts(filter: { sizeField: { neq: 3 } }) {
    title
  }
}
```

#### Filter by `smartTags`

`contains`

Filter uploads linked to the specified tag

```graphql
query {
  allProducts(filter: { smartTagsField: { contains: "bike" } }) {
    title
  }
}
```

`all_in`

Filter uploads linked to all of the specified tags

```graphql
query {
  allProducts(filter: { smartTagsField: { allIn: ["bike"] } }) {
    title
  }
}
```

`any_in`

Filter uploads linked to at least one of the specified tags

```graphql
query {
  allProducts(filter: { smartTagsField: { anyIn: ["bike"] } }) {
    title
  }
}
```

`not_in`

Filter uploads not linked to any of the specified tags

```graphql
query {
  allProducts(filter: { smartTagsField: { notIn: ["bike"] } }) {
    title
  }
}
```

`eq`

Search for uploads with an exact match

```graphql
query {
  allProducts(filter: { smartTagsField: { eq: ["bike"] } }) {
    title
  }
}
```

#### Filter by `tags`

`contains`

Filter uploads linked to the specified tag

```graphql
query {
  allProducts(filter: { tagsField: { contains: "bike" } }) {
    title
  }
}
```

`all_in`

Filter uploads linked to all of the specified tags

```graphql
query {
  allProducts(filter: { tagsField: { allIn: ["bike"] } }) {
    title
  }
}
```

`any_in`

Filter uploads linked to at least one of the specified tags

```graphql
query {
  allProducts(filter: { tagsField: { anyIn: ["bike"] } }) {
    title
  }
}
```

`not_in`

Filter uploads not linked to any of the specified tags

```graphql
query {
  allProducts(filter: { tagsField: { notIn: ["bike"] } }) {
    title
  }
}
```

`eq`

Search for uploads with an exact match

```graphql
query {
  allProducts(filter: { tagsField: { eq: ["bike"] } }) {
    title
  }
}
```

#### Filter by `title`

`matches`

Filter uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      titleField: {
        matches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`not_matches`

Exclude uploads based on a regular expression

```graphql
query {
  allProducts(
    filter: {
      titleField: {
        notMatches: { pattern: "bi(cycl|k)e", caseSensitive: false }
      }
    }
  ) {
    title
  }
}
```

`eq`

Search the asset with the specified title

```graphql
query {
  allProducts(filter: { titleField: { eq: "bike" } }) {
    title
  }
}
```

`neq`

Exclude the asset with the specified title

```graphql
query {
  allProducts(filter: { titleField: { neq: "bike" } }) {
    title
  }
}
```

`in`

Search assets with the specified as default title

```graphql
query {
  allProducts(filter: { titleField: { in: ["bike"] } }) {
    title
  }
}
```

`not_in`

Search assets that do not have the specified as default title

```graphql
query {
  allProducts(filter: { titleField: { notIn: ["bike"] } }) {
    title
  }
}
```

`exists`

Filter assets with the specified field defined (i.e. with any value) or not

```graphql
query {
  allProducts(filter: { titleField: { exists: true } }) {
    title
  }
}
```

#### Filter by `type`

`eq`

Search uploads with the specified type

```graphql
query {
  allProducts(filter: { typeField: { eq: image } }) {
    title
  }
}
```

`neq`

Exclude uploads with the specified type

```graphql
query {
  allProducts(filter: { typeField: { neq: image } }) {
    title
  }
}
```

`in`

Search uploads with the specified types

```graphql
query {
  allProducts(filter: { typeField: { in: [image] } }) {
    title
  }
}
```

`not_in`

Search uploads without the specified types

```graphql
query {
  allProducts(filter: { typeField: { notIn: [image] } }) {
    title
  }
}
```

#### Filter by `width`

`gt`

Search all assets larger than the specified width

```graphql
query {
  allProducts(filter: { widthField: { gt: 3 } }) {
    title
  }
}
```

`lt`

Search all assets smaller than the specified width

```graphql
query {
  allProducts(filter: { widthField: { lt: 3 } }) {
    title
  }
}
```

`gte`

Search all assets larger or equal to the specified width

```graphql
query {
  allProducts(filter: { widthField: { gte: 3 } }) {
    title
  }
}
```

`lte`

Search all assets larger or equal to the specified width

```graphql
query {
  allProducts(filter: { widthField: { lte: 3 } }) {
    title
  }
}
```

`eq`

Search assets with the specified width

```graphql
query {
  allProducts(filter: { widthField: { eq: 3 } }) {
    title
  }
}
```

`neq`

Search assets that do not have the specified width

```graphql
query {
  allProducts(filter: { widthField: { neq: 3 } }) {
    title
  }
}
```

---

# Content Delivery API — SEO and favicon fields

Source [docs]: https://www.datocms.com/docs/content-delivery-api/seo-and-favicon.md

While you can fetch the content of a ["SEO and Social" field](/docs/content-modelling/seo-fields.md) just like any other field, the GraphQL API exposes on every record a much simpler `_seoMetaTags` field that you can use to easily get HTML meta tags based on the information present in the record itself:

```graphql
{
  blogPost {
    _seoMetaTags {
      tag
      attributes
    }
  }
}
```

## How are `_seoMetaTags` generated?

Meta tags are generated merging the values present in the record's "SEO and Social" field, together with the [global SEO Preferences](/docs/content-modelling/seo-fields.md#global-seo-preferences) that you can configure in the Content tab.

If the record doesn't have a "SEO and Social" field, the method tries to guess reasonable values by inspecting the other fields of the record (single-line strings and images).

**`title,`****`og:title`****,** **`twitter:title`**

These titles can be explicitly set in the "SEO and Social" field, if present. If the record does not have that SEO field, or the title is not specified, the tags will be generated from either the record title or the title provided in the global SEO settings.

The *Title suffix* value from global SEO preferences will also be concatenated to the `title`field, as long as the total length of the title + suffix is 60 characters or less. If the combined length is longer, the suffix will be omitted.

The suffix will NOT be added to the OpenGraph and Twitter titles, since there are already other fields for that (`og:site_name` and `twitter:site`).

If needed, you can manually query for the suffix:

```graphql
_site {
  globalSeo {
    titleSuffix
  }
}
```

**`description`****,** **`og:description`****,** **`twitter:description`**

These tags are generated using the description field in the "SEO and Social" field. If no such field is present, or the description is not specified, the tags will be generated from the description specified in the global SEO settings.

`**og:image**`**,** `**og:image:width**`**,** `**og:image:height**`**,** **`og:image:alt`****,** `**twitter:image**`**,** `**twitter:image:alt**`

These tags are generated using the image field in the "SEO and Social" field. If no such field is present, or the image is not specified, the tags will be generated from the image specified in the global SEO settings.

**`robots noindex`**

A `robots noindex` tag will be added if either global SEO Preferences or the "SEO and Social" field have a "Prevent from being indexed by search engines" enabled.

**`og:locale`**

This tag is generated using either the locale specified in the query filter, or the main locale.

**`og:type`**

If the model is a singleton an `og:type` of type `website` will be returned, otherwise an `og:type` of type `website` will be returned.

**`og:site_name`**

The tag is generated from the site name attribute (if provided)

**`twitter:site`**

The tag is deduced from the twitter\_account attribute (if provided)

**`twitter:card`**

The tag is generated from using the twitter\_card field in the "SEO and Social" field. If no such field is present, the global SEO settings will be used.

**`article:modified_time`**

This tag is generated using the updated\_at meta attribute of the Record

**`article:publisher`**

The tag is deduced from the facebook\_page\_url attribute (if provided)

## SEO title & image fallback rules (with automatic fallback)

For SEO purposes, our basic philosophy is that having *some* relevant SEO is better than none at all. Thus, if a record is missing an explicit SEO title or image, **we will try to automatically** ***guess*** **a value for you using the record's own content**. This may be surprising at first, because this automatic fallback happens *before* your project-level SEO preferences are consulted. This section describes how this automatic fallback chain works.

### How SEO title fallback works

We check these places, in order, and pick the **first non-blank value**:

1.  **We always prefer the record's own SEO field**, if it has one, and if its title is set. If the record has 2+ SEO fields, we only look at the first field (alphabetically by API key) and ignore the others.
    
2.  **Otherwise, we automatically pick a fallback field**, in this order:
    
    -   The **"SEO fallback title" field in the model settings**, under the SEO tab, if one is set
        
    -   The first (by API key) **single-line string field with "Show as heading?"** enabled in its field settings (under the Presentation tab)
        
    -   The first **single-line string field** (regardless of its Presentation settings).
        
    
    First we pick that field, **then we check whether it has a non-blank value**. If so, we use it. If not, we skip all other fields and proceed to the next step.
    
3.  **Last resort: the project's own "SEO preferences"** (in the left sidebar). We only check here if ALL the previous checks failed.
    
4.  If all of the above are empty, we altogether omit `title` from the `_seoMetaTags` array.
    

### How SEO image fallback works

We check these places, in order, and pick the **first usable image** (see below for what "usable" means):

1.  **We always prefer the record's own SEO field**, if it has one, and if its image is set. If the record has 2+ SEO fields, we only look at the first field (alphabetically by API key) and ignore the others.
    
2.  **Otherwise, we automatically pick a fallback field**, in this order:
    
    -   The **"Fallback social card image" field designated in the model settings**, under the SEO tab, if there is one.
        
    -   The first (by API key) **asset or gallery field with its "Accept only specified extensions" validation set to "Image (including SVG)"**. (The "Raster image, transformable by imgix" option does not count here.)
        
    -   The first **asset or gallery field** (regardless of its validations).
        
    
    First we pick that field, **then we check whether it has a usable image or video thumbnail**. If so, we use it. If not, we skip all other fields and proceed to the next step.
    
3.  **Last resort: the project's own "SEO preferences"** (in the left sidebar). We only check here if ALL the previous checks failed.
    
4.  If all of the above are empty, we altogether omit `og:image`, `og:image:width`, `og:image:height`, `og:image:alt`, `twitter:image` and `twitter:image:alt` from the `_seoMetaTags` array.
    

**What counts as a usable image:** the asset must be an image (including SVGs) or a video with a thumbnail. At any step, if the asset is another type (PDF, audio file, a video that's still awaiting processing and thumbnail creation, etc.), we skip it and move on to the next step, exactly as if it were empty.

## SEO description fallback rules

The fallback system for the SEO description is much simpler, compared to the system for titles and images above. We check, in order, for the first non-empty value:

1.  The record's **SEO field description**
    
2.  The **model's designated excerpt field,** if one is set in the model's SEO tab. We convert this to plain text and truncate it 200 characters.
    
3.  The sitewide default description set in the main sidebar's **SEO preferences** settings.
    
4.  If none of the above have a value, the `description` tag is dropped from the `_seoMetaTags` array.
    

## Favicon meta tags

Similarly, you can get the meta tags needed to properly show the site's favicon with the `_faviconMetaTags` attribute contained inside the `_site` field:

```graphql
{
  _site {
    faviconMetaTags {
      tag
      attributes
    }
  }
}
```

## iOS and MS app icons

If you're building an app, you can request additional meta tags with the `variants` argument:

```graphql
{
  _site {
    faviconMetaTags(variants: [icon, appleTouchIcon, msApplication]) {
      tag
      attributes
    }
  }
}
```

See an example of how the [SEO meta tags are generated in Next.js](/docs/next-js/seo-management.md).

Want to know more about SEO customization in DatoCMS? Check out this video tutorial:

[

(Image content)

Working with and customizing SEO Fields

Play video »

](https://youtu.be/WjF10isSjS0)

---

# Content Delivery API — Meta fields

Source [docs]: https://www.datocms.com/docs/content-delivery-api/meta-fields.md

## Record meta fields

Every record has some *meta* fields that are providing some meta information on the records.

For example you can get the creation date, the status, etc. All these fields are prefixed with an underscore, let's see them in detail:

-   `_createdAt`: date of creation of the record;
-   `_firstPublishedAt`: date of first publication of the record;
    
-   `_isValid`: is the record valid? This can be false if the schema has changed and the records haven't been updated yet;
-   `_modelApiKey`: the API key of the model;
    
-   `_publicationScheduledAt`: if the publication of a record is scheduled in the future, this field will hold the publication date;
-   `_seoMetaTags`: it's an object with the SEO meta tags computed from an optional SEO field and the fallback details from the main site settings. It's an object representing the meta tags:
    
    -   `attributes`: the meta tag attributes;
        
    -   `content`: the meta tag content;
        
    -   `tag`: the meta tag name;
        
-   `_status`: represent the record status: draft/published;
-   `_updatedAt`: it's the date of last update;
    

All these fields are read-only (also use the CMA) as they either represent an internal state of the record or they are precomputed by our API using other records (e.g., SEO fields).

## Site meta fields

The `_site` object has a site-level meta field:

-   `locales`: the list of available locales.

---

# Content Delivery API — Cache Tags Overview

Source [docs]: https://www.datocms.com/docs/content-delivery-api/cache-tags.md

DatoCMS Cache Tags help optimize your website or app's caching. They allow developers to simply tag webpages with unique identifiers, so when the content from the CMS is updated, these **tags can trigger an immediate and precise cache invalidation** only for the pages that actually include that content, and need to be regenerated.

The main benefits include:

-   **Visitors can instantly view the most updated version of the content**, while maintaining the benefits of completely static and cached content.
-   **Hosting expenses and DatoCMS resource usage can be dramatically reduced** thanks to a precise caching mode that does not rely on time-based invalidation methods, or a total invalidation of the entire site when anything changes.
    
-   **It entirely relieves the developer of the duty to manage cache invalidation,** a task which is instead taken care of by DatoCMS itself.
    

For a more comprehensive understanding of DatoCMS cache tags and the problem it solves, we recommend reading the [**feature's announcement**](https://www.datocms.com/blog/introducing-datocms-cache-tags.md) which provides some additional background.

## How does it work?

Implementing cache tags on your app is a three-step process:

1.  Ask the Content Delivery API to return the cache tags associated with each query, by adding an `X-Cache-Tags` header to your GraphQL requests;
    
2.  Mark every page your website produces with the cache tags you received;
    
3.  Implement an endpoint that invalidates those tags when DatoCMS sends them to you through a webhook.
    

All three steps are designed to be quite straightforward to implement, allowing you to benefit from the advantages this method offers in a very short time. The rest of this section covers them in detail:

-   [**Cache tags in CDA responses**](/docs/content-delivery-api/cache-tags-format.md) — how to request cache tags, how they are encoded, and the limits every response respects.
-   [**Integrating cache tags in your project**](/docs/content-delivery-api/cache-tags-integrations.md) — how to apply the tags and purge them, first with any server behind a CDN, then with popular frameworks and hosting platforms.
    
-   [**The cache tags invalidation webhook**](/docs/content-delivery-api/cache-tags-invalidation.md) — how DatoCMS tells you what to invalidate, and what the payload looks like.
    

## What will be the final cache hit ratio?

It is very difficult to answer this question precisely, as it is connected to a large number of factors including the type of site traffic, the frequency of content updates, the content present in your pages, the GraphQL queries you execute, and the reliability of the cache in the selected framework and hosting.

Sometimes, it's simple to guess which pages will be invalidated when a content change occurs: for instance, if a blog's homepage showcases the latest posts, it's clear that adding a new post on DatoCMS will invalidate the homepage. Another straightforward example: let's say you have a query that pulls content for your website's navigation bar: any pages including that navigation bar need to be invalidated when the query creates new content.

Other cases are less obvious to grasp: suppose that a post can belong to some categories, maybe more than one category. Which are the pages invalidated when an editor changes a post's categories?

So, without being able to predict the actual result in terms of hit ratio, it is certainly possible to say this:

-   Regardless of the frequency of invalidation, a superior result will still be achieved with DatoCMS Cache Tags, compared to redeploying the entire website, invalidating all pages for each individual content change.
-   The benefits of cache tags increase as the number of pages on a website grows.

---

# Content Delivery API — Cache tags in CDA responses

Source [docs]: https://www.datocms.com/docs/content-delivery-api/cache-tags-format.md

Every response from the Content Delivery API can return the list of cache tags associated with the query and its results. This page covers how to request them, how they are encoded, and the limits they respect.

## Requesting cache tags

To access the cache tags, simply add the following header to your existing GraphQL POST requests:

```plaintext
X-Cache-Tags: true
```

With this new header included (and the use of the `--include` flag to show HTTP headers), a CURL request would look like this:

```plaintext
$ curl 'https://graphql.datocms.com/' \
    -H 'Authorization: YOUR-API-TOKEN' \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'X-Cache-Tags: true' \
    --include \
    --data-binary '{ "query": "query { allPosts { title } }" }'
```

> [!PROTIP]
> The `X-Cache-Tags` is one of many headers you can use to shape up the behavior of the Content Delivery API. Refer to the related section for more information on the other [available headers in the Content Delivery API](/docs/content-delivery-api/api-endpoints.md) endpoint.

The response (omitting what's not related to cache tags) will include a new `X-Cache-Tags` header:

```http
HTTP/2 200
...
X-Cache-Tags: BQD?* 2.a*q f7e N*r;L 6-KZ@ t#k[uP t#k[ub t#k[uU
...

{
  "data": {
    "allPosts": [ ... ]
  }
}
```

The `X-Cache-Tags` that appears in the response is a space-separated list of strings: each string represents a cache tag, carefully generated to cover all possible invalidation scenarios.

> [!POSITIVE] Cache tags are not readable, and that's a good thing!
> DatoCMS provides cache tags that are intentionally opaque, to prevent misinterpretation and misuse on your end. Cache invalidation is a complicated process with a high possibility of errors and overlooking specific edge-cases. Our cache tags help us handle these complexities for you. Their non-transparent nature also allows us the flexibility to improve our tagging strategies in the future, without necessitating changes on your frontend.

## Encoding

Cache tags are supposed to be opaque to the user, which means you don't have to know the meaning conveyed by each tag to use it. However, it may be useful to know and consider the encodings of the tags so that you can make sure they work properly across your tech stack.

Each tag is a string encoded using an alphabet of 66 symbols:

```plaintext
!"#$%&@'()*+-./0123456789:;<=>?[\]^_abcdefghijklmnopqrstuvwxyz{|}~
```

Note that the alphabet contains **no spaces, no commas, and no uppercase letters**. Tags are therefore safe to concatenate into the comma-separated or space-separated header formats used by every major CDN, and they stay unambiguous on CDNs that treat cache tags case-insensitively (such as Netlify).

> [!NOTE] Potential future encoding updates
> We strive to ensure our cache tags are compatible with as many CDNs as possible. If we need to modify the encoding to support additional CDNs in the future, we'll handle the transition smoothly. Should such a change occur, we'll automatically send an invalidation event through your existing webhook configuration, allowing your system to adapt without any manual intervention required.

## Limits

To ensure cache tags work across all major CDNs, DatoCMS enforces two limits on every Content Delivery API response:

-   **A maximum of 500 cache tags per response.** This is the lowest common denominator among the CDNs that support tag-based invalidation.
-   **A maximum of ~14 KB for the serialized** **`X-Cache-Tags`** **header.** Most CDNs cap the size of individual response headers (Cloudflare, for example, allows 16 KB per header and 32 KB in total), and we leave room for the rest of your response headers.
    
-   If a response would exceed either limit, our server will intelligently replace some specific tags with less granular, "catch-all" tags. This behavior ensures that your content does not become stale, at the expense of the occasional over-invalidation. Our rationale is that it's generally better to spend a few more API calls to fetch a bit more content than necessary, than to have orphaned stale content that is never correctly invalidated.
    

###### Granularity of cache tags

A response's cache tags cover all the content its query returns, so the more content a query returns, the more often that response is invalidated.

Count the tags in the `X-Cache-Tags` header to see where a query stands. A response that comes back with the maximum of 500 tags is the case worth acting on: it will be invalidated substantially more often than its content actually requires. Fetching fewer records per query — by paginating, or by splitting one large query into several smaller ones — makes invalidation more precise.

---

# Content Delivery API — The cache tags invalidation webhook

Source [docs]: https://www.datocms.com/docs/content-delivery-api/cache-tags-invalidation.md

Once your pages carry cache tags, you need a way to invalidate them when editors change content. In implementing a caching mechanism, this is traditionally the most complex step to tackle.

Fortunately, DatoCMS handles the complex job of tracking every possible alteration in your schema, text, images, and videos for you. When any change happens, DatoCMS can immediately send a list of tags that need invalidation to your frontend through a single webhook.

## Setting up the webhook

Within your Project Settings, create a new webhook. Choose the "Invalidate" event of the "Content Delivery API Cache Tags" entity as the trigger:

(Video content)

The requests that the webhook will send will be in this JSON format:

```json
POST /your/invalidation/endpoint HTTP/1.1
Content-Type: application/json

{
  "entity_type": "cda_cache_tags",
  "event_type": "invalidate",
  "entity": {
    "id": "cda_cache_tags",
    "type": "cda_cache_tags",
    "attributes": {
      "tags": ["N*r;L", "6-KZ@", "t#k[uP"]
    }
  },
  "related_entities": []
}
```

> [!WARNING] Invalidation batches can be large
> DatoCMS groups invalidations over a short window, so the `tags` array has no fixed upper bound: a bulk publish can easily produce several hundred tags in a single delivery.
> 
> Most purge APIs cap how many tags one request may carry, and rate-limit how often you can call them. Chunk the array to your provider's per-request limit, and retry on rate-limit responses. For reference, Cloudflare accepts 100 tags per purge request on Business plans.

## Implementing the endpoint

The final step is to implement the endpoint that will receive incoming requests from the webhook. The task of this endpoint will be to execute cache invalidation based on the received cache tags.

The way you perform cache invalidation through tags **greatly depends on the frontend framework and hosting solution you use**. In some instances it's an API call, whereas some frameworks offer specific helper functions. The integration guides cover both cases with working code.

###### Don't forget to invalidate on deploy

When there's a cache layer above your application, content changes are not the only reason a cached page can go stale: a new version of your application will produce different HTML for the same content. Deploys are far less frequent than content changes, so a full purge of the CDN cache as the last step of your deploy pipeline is usually all you need.

---

# Content Delivery API — Integrating cache tags in your project

Source [docs]: https://www.datocms.com/docs/content-delivery-api/cache-tags-integrations.md

Whatever your stack, an integration comes down to two things: marking each cached artifact with the tags that the Content Delivery API returned, and purging those tags when the invalidation webhook fires. How you do them depends on what sits between your code and your visitors.

This page starts with the general case, any server behind any CDN, which is the simplest to reason about and the clearest way to see the mechanism. The second half covers popular frameworks and hosting platforms, where the picture is less uniform.

## Any server, any CDN

If your application can set custom HTTP headers on a per-page basis, then regardless of language or framework you can use cache tags by **placing a CDN that supports tag-based invalidation on top of it.**

###### What is tag-based cache invalidation?

Tag-based cache invalidation is a method where keywords (tags) can be assigned to cached pages. This technique is provided by all the major content delivery services such as [Netlify](https://www.netlify.com/blog/cache-tags-and-purge-api-on-netlify/), [Fastly](https://docs.fastly.com/en/guides/working-with-surrogate-keys), [Bunny](https://bunny.net/blog/introducing-tag-based-cdn-cache-purging/) and [Cloudflare](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/). The mechanism:

-   **Assign tags:** When your application delivers a page, it lists a series of tags in a response header (the header's name depends on the CDN). Each tag labels a piece of the content on that page.
-   **Caching:** The CDN stores the response under its primary cache key, the URL, together with the associated tags.
    
-   **Purging:** When content behind a tag changes, you can easily drop every cached item carrying that tag in a single call.
    

Different services name the same concept differently. Fastly calls cache tags "Surrogate Keys". The header your application uses to declare the tags varies too: Netlify and Cloudflare read `Cache-Tag`, Bunny reads `CDN-Tag`. Netlify also accepts `Netlify-Cache-Tag`, which is preferable there because Netlify strips it from the response before it reaches the browser. What this documentation calls *"cache invalidation"*, other services call *"cache purge"*.

Check your CDN's documentation for the exact details, format, and limitations.

###### The whole integration, end to end

The integration is small enough to show in full. The example below uses [Hono](https://hono.dev/) as the server and [Fastly](https://docs.fastly.com/en/guides/working-with-surrogate-keys) as the CDN, but nothing in it is specific to either: the same two response headers and the same purge call apply to Express, Fastify, Rails, Laravel, or anything else that can set a header.

**Tagging a response.** Ask the Content Delivery API for cache tags, then forward them to the CDN:

```javascript
import { Hono } from 'hono';
import { rawExecuteQuery } from '@datocms/cda-client';

const app = new Hono();

app.get('/posts/:slug', async (c) => {
  const [data, response] = await rawExecuteQuery(POST_QUERY, {
    token: process.env.DATOCMS_CDA_TOKEN,
    returnCacheTags: true,
    variables: { slug: c.req.param('slug') },
  });

  const cacheTags = response.headers.get('x-cache-tags');

  if (cacheTags) {
    c.header('Surrogate-Key', cacheTags);
    c.header('Surrogate-Control', 'max-age=31536000');
  }

  return c.html(renderPost(data));
});
```

Two details in there:

-   `X-Cache-Tags` is space-separated, and so is Fastly's `Surrogate-Key`: the value can travel verbatim. CDNs that expect a comma-separated `Cache-Tag` need `cacheTags.split(' ').join(',')` instead.
-   `Surrogate-Control` tells the CDN how long to keep the response, and Fastly strips it before the response reaches the browser. A one-year lifetime on the CDN is therefore safe, since cache tags rather than time expire the entry. Add a regular `Cache-Control` header, with a much shorter lifetime, if you also want visitors' browsers to cache the page.
    

**Purging on content change.** The webhook endpoint receives the tags and hands them to the CDN's purge API:

```javascript
app.post('/api/invalidate-cache', async (c) => {
  if (c.req.header('authorization') !== `Bearer ${process.env.WEBHOOK_TOKEN}`) {
    return c.json({ success: false }, 401);
  }

  const { entity } = await c.req.json();
  const { tags } = entity.attributes;

  const response = await fetch(
    `https://api.fastly.com/service/${process.env.FASTLY_SERVICE_ID}/purge`,
    {
      method: 'POST',
      headers: {
        'fastly-key': process.env.FASTLY_KEY,
        'content-type': 'application/json',
      },
      body: JSON.stringify({ surrogate_keys: tags }),
    },
  );

  return c.json({ success: response.ok }, response.ok ? 200 : 502);
});
```

The integration ends there: two response headers on the way out, and one POST when content changes.

## Popular frameworks and hosting platforms

Some frameworks offer their own caching layer, with helpers that work across hosting providers instead of raw HTTP headers. When one is available it can be the more idiomatic path, but frameworks differ in how well they carry a set of cache tags from end to end, and that difference is worth knowing before you commit to a stack.

We cannot document every framework and hosting combination. We maintain guides for the setups people ask us about most, and for the ones we are most comfortable recommending.

###### What makes a stack a good fit

Three questions tell you most of what you need to know about a combination we haven't documented:

-   **Does it carry all your tags?** A Content Delivery API response can carry up to 500 cache tags, and the ceiling can come from either the hosting platform or the framework. Among platforms, Cloudflare's `Cache-Tag` header holds roughly 1,000 and Netlify accepts exactly 500, both above what we emit, as is Fastly's `Surrogate-Key`; [Vercel caps a cached response at 128 tags](https://vercel.com/docs/caching/cdn-cache/purge), well below our maximum. Among frameworks, Next.js applies that same figure one layer up: [it associates at most 128 tags with each `fetch()`](https://nextjs.org/docs/app/api-reference/functions/fetch#optionsnexttags), so the limit travels with the framework even when you host it elsewhere.
-   **Does it need extra infrastructure?** When tags travel verbatim from our response header into the CDN's header, nothing else is required. When they don't fit, you need somewhere to store the mapping between what you tagged and what we invalidate, which means a database in the request path.
    
-   **How fast can it invalidate?** Purge APIs cap the tags per request and rate-limit the calls. Cloudflare accepts 100 tags per purge request on every plan, at 5 requests per minute on Free up to 50 per second on Enterprise. Netlify allows a purge twice every five seconds, per tag or per site. Vercel's bulk endpoint accepts 16 tags per call, the tightest of the three. Since a bulk publish can produce several hundred tags in one webhook delivery, plan on chunking and retrying.
    

###### Where the friction shows up

Next.js is the clearest example of the second question mattering. Since a `fetch()` accepts at most 128 tags, you cannot hand DatoCMS tags to Next.js directly. Our guide works around this by tagging each query with a single synthetic identifier and keeping a "query ID to cache tags" mapping in a persistent database, written whenever the cache is filled and read whenever the webhook fires. The approach works, we document it in full, and a starter project comes with it, but you will run and operate that database, and it sits in the invalidation path. The two costs belong to different layers: you pay for the mapping layer because of the framework, while the 128-tag ceiling is Vercel's and stays where it is no matter which framework renders the page.

Stacks where the tags reach the CDN untouched avoid the mapping layer. Astro is a good example on the framework side: its Cache API takes `context.cache.set({ maxAge, tags })` and `cache.invalidate({ tags })`, and the adapter's cache provider forwards the tags to the platform's own header and purge API, with nothing in between. You end up with the same shape as the plain-HTTP example above, minus the platform details. Providers exist for Cloudflare, Netlify and Vercel alike, though on Vercel you keep the simplicity and still inherit the 128-tag ceiling.

The hosting platform decides most of this, not the framework. Cloudflare, Netlify and Fastly all carry more tags than we emit and expose a purge-by-tag API that any code can call, so on those platforms *any* framework able to set a response header can integrate cache tags. That is the first half of this page, applied to a managed platform instead of your own CDN. A framework adds ergonomics on top: with Astro you never touch a header or a purge endpoint, because its providers wrap those same platform APIs for you.

If you are still choosing, we would point you toward a platform that keeps cache tags verbatim from end to end and leaves room for all 500 of them, so Cloudflare, Netlify or Fastly, and then pick whichever framework you would have picked anyway. Both costs described above are worth avoiding when you can: a mapping layer is infrastructure you operate, and a tag ceiling below 500 is a limit you will hit one day without anyone telling you.

###### Our guides

Each of these walks through one stack end to end, from the first query to the invalidation webhook:

-   [DatoCMS Cache Tags and Astro](/docs/astro/using-cache-tags.md): built on Astro's Cache API, and covering what changes between Cloudflare, Netlify and Vercel.
-   [DatoCMS Cache Tags and Next.js](/docs/next-js/using-cache-tags.md): including the mapping layer that the 128-tag ceiling makes necessary.
    

If yours isn't here, you are not stuck: the three questions above will tell you most of what to expect from it, and the plain-HTTP integration at the top of this page works with any server that can set a response header, behind any CDN that can purge by tag.

---

# Content Delivery API — Error codes & handling failures (CDA)

Source [docs]: https://www.datocms.com/docs/content-delivery-api/errors.md

### Content Delivery API Errors and Failure Modes

CDA errors happen when your frontend fails to query our GraphQL Content Delivery API for any reason.

This can occur at different parts of the network stack, with different kinds of errors and responses, detailed below.

> [!NOTE] These errors are only for the GraphQL Content Delivery API
> If you're looking for errors related to our REST Content Management API, please instead see: [Error codes & handling failures (CMA)](/docs/content-management-api/errors.md)

## Network Errors

**Network errors** occur when your request never made it to our servers. This can be due to a Wi-Fi problem, misconfigured VPN, corporate firewall, regional network outage, browser or HTTPS issue, etc. Rarely, it might also indicate server outages and downtime on our part. You can always check out status page at [https://status.datocms.com/](https://status.datocms.com/) or the [Outages section of the DatoCMS forum](https://community.datocms.com/c/outages/25).

## GraphQL Query Errors

**GraphQL query errors** occur when the request reached our GraphQL server OK, but there was something wrong with the query itself.

> [!WARNING] GraphQL query errors will still return a HTTP 200 OK
> If a malformed or otherwise invalid query reaches our GraphQL server, you'll still receive a `**200 OK**` **HTTP status.** But that doesn't mean the query succeeded, only that our server received it. **The HTTP status is NOT a way to see if a query succeeded. Instead, you must check for the possible presence of an** **`errors[]`** **array.**

A **successful** CDA response looks like:

```json5
// Successful CDA responses will have a data[] array and no errors[] array.
// It will have a HTTP 200 OK status.

{
  "data": {
    "allArticles": [
      {
        "id": "abcdefghji12345",
        "title": "This is an example article",
        "slug": "example-article"
      }
    ]
  }
}
```

A **failed** CDA response looks like:

```json5
// Failed CDA queries will return an errors[] array instead of data[]
// Failed queries will ALSO have a HTTP 200 OK status. DO NOT TRUST THAT!

{
  "errors": [
    {
      "message": "Field 'Sku' doesn't exist on type 'ArticleRecord'",
      "locations": [
        {
          "line": 16,
          "column": 5
        }
      ],
      "path": [
        "query",
        "allArticles",
        "Sku"
      ],
      "extensions": {
        "code": "undefinedField",
        "typeName": "ArticleRecord",
        "fieldName": "Sku"
      }
    }
  ]
}
```

`**Within an errors[]**` **array**, each object will have the following properties:

-   `message`: The human-readable error message.
-   `locations`: The query line and column # where the server thinks the error occurred. Note that because of formatting and line break differences, the precise location may be slightly different in your code.
    
-   `path`: The attempted GraphQL path (e.g. `query`.`modelName`.`fieldName`) that caused the error.
-   `extensions`: Extended DatoCMS-specific errors that we provide to try to help you diagnose what went wrong. May be different for different kinds of query errors.
    

### HTTP & API Errors

**HTTP & API errors** occur when the network request itself has an issue. The most common examples are invalid authorization tokens or hitting the rate limit on uncached queries.

An HTTP or API error will have a shape similar to this:

```json5
{
  "id": "abcde12345",
  "type": "api_error",
  "attributes": {
    "code": "INVALID_JSON_BODY", // Machine-parseable code
    "details": {
      "message": "The JSON body you submitted is not a valid GraphQL request" // For humans
    }
  }
}
```

`attributes.code` will be the machine-readable error code. See below for a list.

`attributes.details.message` will be a short, human-readable explanation.

### List of HTTP & API Error Codes

###### **INVALID\_AUTHORIZATION\_HEADER**

This error occurs when the provided API Authorization header is invalid or absent. Ensure that the API token used in the request is valid, has appropriate Content Delivery API (CDA) access permissions, and that the header is properly formatted.

###### **INVALID\_ENVIRONMENT**

This error occurs when the GraphQL API request targets an environment that doesn’t exist. Check your environment identifier in the request.

###### INVALID\_JSON\_BODY

The JSON request body is itself malformed or invalid, and our server can't find your query in the request. Perhaps you missed a bracket? Please see [Using the JavaScript CDA client](/docs/content-delivery-api/your-first-request.md) or use the "Playground" in your project, along with your browser's network inspector, to see what a properly-formed request would look like.

###### **ENVIRONMENT\_NOT\_READY**

This error occurs when attempting to access an environment that exists but is not in a “ready” state. To resolve this, ensure that the environment you’re targeting has transitioned to “ready” status. You can check the current environment’s status via the DatoCMS interface or API before making modification requests.

###### **DEACTIVATED\_SITE**

This error occurs when attempting to access a site that has been deactivated. To fix this, go to the DatoCMS dashboard and address any pending billing issues.

###### **SITE\_NOT\_READY**

This error occurs when attempting to access a site that exists but is not in a “ready” state. The site may be initializing. Verify that the desired project is accessible, activated, and ready.

###### **INSUFFICIENT\_PERMISSIONS**

This error occurs when a valid API token exists but lacks the necessary permissions to access the requested environment. The authentication succeeds, but the token doesn’t have the required authorization level for the operation. Ensure your API token has the appropriate role and permission settings for the environment you’re trying to access.

###### **INVALID\_X\_INCLUDE\_DRAFTS\_HEADER**

This error occurs when the X-Include-Drafts header in your GraphQL API request has an invalid value. The header can only be set to “true” to include draft content in the response. Ensure your API request uses the correct value for this header or omit it entirely if you don’t need draft content.

###### **INVALID\_X\_EXCLUDE\_INVALID\_HEADER**

This error occurs when the X-Exclude-Invalid header in your GraphQL API request has an invalid value. The header can only be set to “true” to exclude invalid content items from the response. Verify that your request uses the correct value for this header or remove it if not needed.

###### **INVALID\_X\_VISUAL\_EDITING\_HEADER**

**(Enterprise Feature)**

This error occurs when the X-Visual-Editing header is provided, but your site doesn’t have visual editing capabilities, which is an enterprise-only feature. Contact [support@datocms.com](mailto:support@datocms.com) for information about upgrading your plan to access this functionality.

###### **INVALID\_X\_VISUAL\_EDITING\_HEADER**

**(Invalid Value)**

This error occurs when the X-Visual-Editing header is provided with an invalid value. Currently, the only supported values for this header are `v1` and `vercel-v1`. Ensure your API request uses the correct value for this header when using visual editing features.

###### **INVALID\_X\_BASE\_EDITING\_URL\_HEADER**

This error occurs when the X-Visual-Editing header is specified but the required X-Base-Editing-Url header is missing. When using visual editing features, you must provide the base editing URL to properly generate editing links. Ensure both headers are properly configured in your request.

---

# Content Delivery API — CDA Technical Limits & Rate Limits

Source [docs]: https://www.datocms.com/docs/content-delivery-api/technical-limits.md

> [!NOTE] Content Delivery API (GraphQL) Only
> These limits apply only to the Content Delivery API, our read-only GraphQL API.
> 
> For other API limits, see:
> 
> -   [CMA Technical Limits & Rate Limits](/docs/content-management-api/technical-limits.md)
>     
> -   [Real-time Updates API Limits & Pricing](/docs/real-time-updates-api/limits-and-pricing.md)

## Overview

To ensure system integrity and performance for all our customers, the Content Delivery API imposes 3 basic technical limits on your queries:

-   **Complexity**: How difficult your query is for our origin server to serve, in terms of its [**GraphQL complexity**](/docs/content-delivery-api/complexity.md) (max `10,000,000`). Queries that exceed this limit will return an error.
-   **Cacheability**: Your query request size, in bytes, indicating whether our CDN *can* cache it (max `12,000` bytes). Uncached or uncacheable requests are subject to rate limits (see below).
    
-   **Concurrency**: How many active, uncached requests our origin server can process for your project at once (normally `40`), shared between all of your requests that miss the cache. This primarily affects slow, long-running queries.
    

Whenever you make a CDA query, it first goes through our CDN, Cloudflare. Per [data center](https://www.cloudflare.com/network/), we will serve the query straight from cache if possible, warm up the cache if needed, or tell you that this query can never be cached (because it's too long). Once it hits our origin server, we also check for its complexity and concurrency, process it, then return either a successful result or an [error](/docs/content-delivery-api/errors.md).

To make sure your queries succeed and aren't limited, please make sure:

1.  Your query's `x-complexity` HTTP header is under the maximum [GraphQL query complexity limit](/docs/content-delivery-api/complexity.md) (`10,000,000`). Requests that exceed this limit will return a `HTTP 200` with a [JSON `errors` object from our GraphQL server](/docs/content-delivery-api/errors.md) with a message like "`Query has complexity of X, which exceeds max complexity of 10000000`".
    
2.  The `x-cacheable-on-cdn` HTTP header should return `true`. Requests that exceed this limit are too long for the CDN to cache and will always be dynamic and rate-limited. Consider breaking it down into smaller, separate queries instead.
    

If all of the above are OK, then the bulk of your CDA responses should include a HTTP header of `cf-cache-status: HIT`, indicating a successful cache hit. For these cache hits, you don't have to worry about rate limits at all (because they only apply to uncached responses).

For troubleshooting, there is a table further down this page with relevant HTTP headers.

### **Rate Limits for Uncached CDA Requests**

Cached requests (see above) are not subject to a rate limit.

Uncached requests must stay within two rate limits, both active simultaneously:

-   **40 requests/second per API token**
-   **1000 requests/min per API token**
    

The rate limits are per API token. If your frontend uses the same API token across many pages, or in a big parallel build, every request made with the same token counts towards the same pool.

> [!PROTIP] Pro tip: Use datocms/cda-client to automatically handle rate limits
> If you use our official CDA client ( [Using the JavaScript CDA client](/docs/content-delivery-api/your-first-request.md) ), it will automatically handle our rate limits for you.
> 
> This is great for simpler use cases, but please note that it does not account for concurrency (parallel Next.js build workers, `Promise.all()` over many simultaneous requests, etc.).

#### **Rate Limit Examples**

1.  If you use our official CDA client ( [Using the JavaScript CDA client](/docs/content-delivery-api/your-first-request.md) ) to make your CDA requests, we handle this for you automatically, and you don't have to worry about the other examples.
    
2.  You make **60 requests** at once and hit the **per-second** **limit** immediately. The last 20 requests will be rejected with a `HTTP 429 Too many requests`. You should retry again after `x-ratelimit-reset: 1` second.
    
3.  You sustain **40 requests/second** and stay within the per-second limit. But after 25 seconds, you will hit the **per-minute limit**. Remaining requests are rejected with a `429`. You should retry again after `x-ratelimit-reset: 35` seconds.
    
4.  You sustain **40 requests/second across two worker threads, 20/sec each**: Same outcome as above. The rate limit is per API token, not per client.
    
5.  You sustain **16 requests/second**, in total across that API token, and stay under both limits indefinitely. Well done!
    

#### Project-wide Concurrency Limit

Separate from the per-token rate limits above, there is also a **per-project limit of** **`40`** **concurrent requests**, **shared between all your API tokens**. "Concurrent" means that those requests are actively being processed by our origin server at the same time, regardless of when we received them.

Whereas rate limits measure only the *frequency* of incoming requests; concurrency measures how many are still being processed at the same time — their overlapping *durations*, in other words.

**An exaggerated example:** If you send us 16 requests/second, you will stay comfortably within the rate limits. But if each of those were a separate, slow and uncached query taking 10+ seconds to look up, then within 3 seconds you'll hit the concurrency limit. The first 40 requests will keep processing for the next 7+ seconds, while the last 8 requests will be rejected with a `429`.

Normally, concurrency is not something the typical project or use case ever needs to worry about. It primarily affects only very slow, long-running queries that are sent at or near the same exact time, AND that are all uncached and hit our origin server. In practice, this combination of factors is extremely rare, and most of our customers never encounter it.

We don't have separate error codes or HTTP headers for concurrency exceptions; they just look like regular rate limiting, and you can respond to them the same way: by simply waiting longer and respecting the `x-ratelimit-reset` header.

However, if you suspect you are regularly hitting the concurrency limit, please first check your query and its filters, variables, and relationships and see if any could be simplified. If you need further help, please [contact DatoCMS Support](https://www.datocms.com/support.md) and we can help you further troubleshoot it on a case-by-case basis.

## **HTTP Headers for CDA Responses**

CDA responses include the following HTTP headers to help you stay within limits and diagnose caching and rate limit issues.

| Category | HTTP Header | Value | Notes |
| --- | --- | --- | --- |
| Complexity | `x-complexity` | `<integer>` This query's GraphQL complexity | Your request's current GraphQL complexity |
| Complexity | `x-max-complexity` | `10000000` Max complexity allowed | The maximum complexity allowed, 10,000,000 by default. |
| Cacheability | `cf-cache-status` | `HIT` or `MISS` or `DYNAMIC` or `BYPASS` Whether the CDN can cache this query | `HIT` indicates a cache hit, the desired behavior. No rate limits apply to this query. `MISS` indicates a cache miss, usually because it was the first time this particular request hit that data center. Repeat the same query/request and it should become a HIT next time. `DYNAMIC` means this query can NEVER be cached and will ALWAYS be subject to rate limits. May be due excess complexity, the query length limit (see below), or other error states. `BYPASS` is a special state that means our origin server told the CDN not to cache it. This should only happen on error states, either on the `429` themselves, OR sometimes on a `200` if there is a GraphQL error from our origin server. (See https://www.datocms.com/docs/content-delivery-api/errors#graphql-query-errors) |
| Cacheability | `x-cacheable-on-cdn` | `true` or `false` | Whether the request body is short enough to ever be cacheable. This is the boolean summary of the following header, `x-cacheable-on-cdn-query-length-limit`. |
| Cacheability | `x-cacheable-on-cdn-query-length-limit` | `<integer>/12000` Compressed query size, in bytes, out of allowed max | The actual calculation that determines if the previous header, `x-cacheable-on-cdn`, can ever be true. This is your GZIP-compressed & base64-encoded query size + our overhead, in bytes. The result must stay within 12,000 bytes (12KB decimal). |
| Rate Limits | `x-ratelimit-limit` | `40` or `1000` Which rate limit bucket you're currently closer to. | `40`: You're closer to the per-second rate limit `1000`: You're closer to the per-minute rate limit |
| Rate Limits | `x-ratelimit-remaining` | `<integer>` Requests remaining in closest bucket | How many requests you have remaining in the current bucket (per-second or per-minute), depending on `x-ratelimit-limit`. |
| Rate Limits | `x-ratelimit-reset` | `<integer>` Seconds until next bucket refill | `1` for the per-second limit < `60` for the per-minute limit This header is only added when rate-limited, i.e., accompanying a `HTTP 429 Too many requests` status code. |
| Miscellaneous | `x-request-id` | `UUIDv4` Internal ID that helps us troubleshoot specific requests | Please include this in support requests that refer to a specific GraphQL query. |

## Billing considerations

Caching does not impact billing or quota calculations.

1 request = 1 API call billed against your quota, regardless of whether it's cached or cacheable.

Free plans that exceed CDA limits will be temporarily suspended until the next month. Paid plans that exceed their included CDA quota will be charged per-request overages (in chunks). See [Pricing](https://www.datocms.com/pricing.md) for details.

## Notes

1.  Very rarely, extreme load on our origin servers can also cause separate `429 Too many requests` errors, altogether separate from your query. You are very unlikely to ever experience this, and the remedy is the same as any other rate limit; please try again in a few seconds.
    
2.  Invalidations happen automatically. When content in your project is changed, our system calculates the relevant queries to invalidate. For more manual control, you may wish to consider [Cache Tags](/docs/content-delivery-api/cache-tags.md).
    
3.  If you get a `cf-cache-status: BYPASS` on a `HTTP 200 OK`, this is probably because you are getting a GraphQL error from our origin server (those errors return a `200` with a JSON body you must parse to see the actual GraphQL error). See [CDA: GraphQL Query Errors](/docs/content-delivery-api/errors.md#graphql-query-errors) for more information.

---

# Content Delivery API — Complexity

Source [docs]: https://www.datocms.com/docs/content-delivery-api/complexity.md

Each query hitting our Content Delivery API has a complexity cost based on

-   which type of field is present
-   how many fields are present
    
-   how many filters are present
-   how many sorting parameters are present
    
-   the page size
    

### Limits

Each DatoCMS plan comes with a maximum allowed complexity cost which is **10,000,000 by default**. Take a look at the "Plan and usage" section of your project in the [Account dashboard](https://dashboard.datocms.com/) to understand which is your complexity limit.

The Content Delivery API sets the `X-Complexity` header in the response to let you know the calculated complexity cost for your submitted query and the `X-Max-Complexity` header containing your current plan complexity limit.

If the query complexity cost above your plan limit **you'll get an error from the Content Delivery API**:

```json
{
  "errors": [
    {
      "message": "Query has complexity of xxx, which exceeds max complexity of yyy"
    }
  ]
}
```

### Base costs

Unless specified otherwise, each requested GraphQL field has a cost of 1.

Each filter argument has a cost of 250, except for [deep filtering arguments](/docs/content-delivery-api/complexity.md#deep-filtering).

Each sorting parameter has a cost of 250.

To calculate the pagination cost, multiply the page size by the inner fields' cost. By default, the page size is 20 but you can override it using the `first` argument.

### Model root fields

#### Collection field

Root fields like `allArtists` have a base cost of 100. To this, the cost of filters, the cost of sorting, and the cost of pagination must be added.

The following query has a cost of 140: 100 (base) + 20 (implicit page size) x 2 (inner fields' cost)

```graphql
query { # it returns at max 20 records by default
  allArtists { # 100
    id # 1
    name # 1
  }
}
```

The following query has a cost of 1,175: 100 (base) + 750 (filtering) + 250 (sorting) + 25 (page size) x 3 (inner fields' cost)

```graphql
query {
  allArtists( # 100
    filter: {
      birth: {gt: "1990-01-01", lt: "2010-01-01"}, # 250 x 2 => 500
      country: {eq: "DE"} # 250
    },
    orderBy: [name_ASC], # 250
    first: 25 # explicit page size
  ) {
    id # 1
    name # 1
    age # 1
  }
}
```

#### Collection meta field

Root fields like `_allArtistsMeta` have a base cost of 1,000. To this, the cost of filters and the inner field's cost must be added.

The following query has a cost of 1,251: 1,000 (base) + 250 (filtering) + 1 (inner fields' cost)

```graphql
query {
  _allArtistsMeta( # 1,000
    filter: { country: {eq: "NL"} } # 250
  ) {
    count # 1
  }
}
```

#### Single record field

Root fields like `artist` have a base cost of 50. To this, the cost of filters, the cost of sorting, and the inner fields' cost must be added.

The following query has a complexity cost of 301: 50 (base) + 250 (filtering) + 1 (inner fields' cost)

```graphql
query {
  artist( # 50
    filter: { id: {eq: "123"} } # 250
  ) {
    name # 1
  }
}
```

#### Single instance record field

Root fields about single-instance records have a base cost of 25. To this, the inner field's cost must be added.

The following query has a cost of 27: 25 (base) + 2 (inner fields' cost)

```graphql
query {
  contactPage { # 25
    phoneNumber # 1
    emailAddress # 1
  }
}
```

#### Inverse relationships fields

Fields like `_allReferencingMovies` can be used once activated the [inverse relationships feature](/docs/content-delivery-api/inverse-relationships.md) for the model. They have a base cost of 100. To this, the cost of filters, the cost of sorting, and the cost of pagination must be added.

The following section has a cost of 1,110: 100 (base) + 500 (filtering) + 500 (sorting) + 5 (page size) x 2 (inner fields' cost)

```graphql
...
    _allReferencingMovies( # 100
      through: {
        fields: {anyIn: [movie_artist]}, # 250
        locales: {anyIn: en} # 250
      },
      orderBy: [title_ASC, _createdAt_DESC], # 500,
      first: 5 # explicit page size
    ) {
      id  # 1
      title # 1
    }
...
```

Fields like `_allReferencingMoviesMeta` have a base cost of 1,000. To this, the cost of filters and the inner fields' cost must be added.

The following section has a cost of 1,001.

```graphql
...
    _allReferencingMoviesMeta { # 1,000
      count # 1
    }
...
```

### Model fields

The following GraphQL fields differ from the base cost (1):

-   Single asset field: 5
-   Asset gallery field: 5 x inner fields' cost
    
-   Multiple-paragraph text, when rendering Markdown in HTML: 5
-   JSON field: 5
    
-   Single link field: 10
-   Multiple links field: 5 x inner fields' cost
    
-   Modular content field: 5 x inner fields' cost
-   Structured text field
    
    -   value: 10
        
    -   blocks: 5 x inner fields' cost
        
    -   links: 5 x inner fields' cost
        
-   `children` field (available in tree collections): 5 x inner fields' cost
-   `parent` field (available in tree collections): 25
    
-   Localized field
    
    -   value: number of environment's locales x inner fields' cost
        
-   SEO field
    
    -   image: 5
        
-   `_seoMetaTags`: 5
    

The following query has a cost of 351, composed by:

-   50 (base cost of single record field)
-   250 (filtering)
    
-   5 + 1 + 5 (photo field)
-   10 + 5 x 1 + 5 x 2 (content field)
    
-   5 x 3 (movies field)
    

```graphql
query {
  artist( # 50
    filter: { id: {eq: "123"} } # 250
  ) {
    photo { #single asset field: 5
      url # 1
      blurUpThumb # 5
    }
    content { # structured text field
      value # 10
      links { # 5 x inner fields' cost
        id # 1
      }
      blocks { # 5 x inner fields' cost
        id # 1
        text # 1
      }
    }
    movies { # multiple links field: 5 x inner fields' cost
      id # 1
      title # 1
      releaseDate # 1
    }
  }
```

### Unions

A GraphQL union complexity is the max complexity between all of the possible types.

The modular content field `content` has a complexity cost of 10: 5 x 2

```graphql
query {
  artist(filter: { id: { eq: "123" }}) {
    name
    content { # 5 x max possible union's cost
      ... on MovieRecord {
        title # 1
      }
      ... on TvSerieRecord {
        title # 1
        channel # 1
      }
    }
  }
}
```

### Deep Filtering

Normally, each filter argument has a cost of 250, but when using [deep filtering](/docs/content-delivery-api/deep-filtering.md) an additional cost of 1,000,000 is added for each type of block model defined in the filter.

The following query has a cost of 2,000,890: 100 (base) + 2,000,750 (filtering) + 20 (implicit page size) x 2 (inner fields' cost)

```graphql
query {
  allBlogPosts( # 100
    filter: {
      content: {
        any: {
          product: { # 1,000,000
            name: {
              eq: "T-Shirt" # 250
            },
            price: {
              gt: 30 # 250
            }
          }
          cta: { # 1,000,000
            title: {
              isPresent: true # 250
            }
          }
        }
      }
    }
  ) {
    id # 1
    title # 1
  }
}
```

### Upload root fields

#### Collection field

Root field `allUploads` has a base cost of 100. To this, the cost of filters, the cost of sorting, and the cost of pagination must be added.

The following query has a cost of 801: 100 (base) + 250 (filtering) + 250 (sorting) + 30 (page size) x 7 (inner fields' cost)

```graphql
query {
  allUploads( # 100
    filter: {format: {eq: "jpg"}}, # 250
    orderBy: [size_DESC], # 250
    first: 30 # explicit page size
  ) {
    id # 1
    url # 1
    blurUpThumb # 5
  }
}
```

#### Collection meta field

Root field `_allUploadsMeta` has a base cost of 1,000. To this, the cost of filters and the inner fields' cost must be added.

The following query has a cost of 1,251: 1,000 (base) + 250 (filtering) + 1 (inner fields' cost)

```graphql
query {
  _allUploadsMeta( # 1,000
    filter: {format: {eq: "jpg"}} # 250
  ) {
    count # 1
  }
}
```

#### Single upload field

Root field `upload` has a base cost of 50. To this, the cost of filters, the cost of sorting, and the inner fields' cost must be added.

The following query has a complexity cost of 308: 50 (base) + 250 (filtering) + 8 (inner fields' cost)

```graphql
query {
  upload( # 50
    filter: {id: {eq: "123"}} # 250
  ) {
    url #1
    title #1
    blurUpThumb # 5
    blurhash #1
  }
}
```

### Upload fields

The following GraphQL fields differ from the base cost (1):

-   blurUpThumb: 5
    

### Site field

Root field `_site` has a base cost of 10. To this, the inner fields' cost must be added.

The following query has a cost of 13.

```graphql
query {
  _site { # 10
    globalSeo { # 1
      siteName # 1
      titleSuffix # 1
    }
  }
}
```

---

# Content Delivery API — Custom Scalar Types

Source [docs]: https://www.datocms.com/docs/content-delivery-api/custom-scalar-types.md

The API references a number of custom GraphQL Scalar Types. If you're using code generators to transform GraphQL types coming from the Content Delivery API to TypeScript, here's the list of mappings you need to specify:

```plaintext
BooleanType: boolean
CustomData: Record<string, string>
Date: string
DateTime: string
FloatType: number
IntType: number
ItemId: string
JsonField: unknown
MetaTagAttributes: Record<string, string>
UploadId: string
```

> [!PROTIP] Pro tip: How To Generate TypeScript Types From GraphQL
> Generating TypeScript types from GraphQL queries improves code security, consistency, and robustness by avoiding manual type definitions. [This tutorial](https://www.datocms.com/blog/how-to-generate-typescript-types-from-graphql.md) explains how to set up graphql-codegen to automatically generate TypeScript types for a Next.js project using DatoCMS.

---

# Content Delivery API — Changelog

Source [docs]: https://www.datocms.com/docs/content-delivery-api/changelog.md

All the changes to the Content Delivery API:

## 2022/06/10 - Add `RecordInterface` and `FileFieldInterface` interfaces

-   Every GraphQL type related records/blocks now implement the `RecordInterface` interface;
-   Every GraphQL type related to uploads, single asset or asset gallery fields now implements the `FileFieldInterface` interface.
    

## 2021/04/12 - Add `isBlank` filter to text fields

To have a simple way to filter empty texts, especially when using a structured text field, we have added a `isBlank` filter to the textual fields.

-   **Changes to item fields**
    
    -   **Single-line text field** Added boolean filter `isBlank`
        
    -   **Multiple-line text field** Added boolean filter `isBlank`
        
    -   **Structured text field** Added boolean filter `isBlank`
        

## 2020/05/11 - Changes in GraphQL filtering

To make the API more consistent and prevent ambiguous results we have changed how filtering works in some edge cases.

This is a big changeset, but should only affect edge cases and the minority of usages, following all the details.

-   **Changes to item fields**
    
    -   **Boolean field**Filtering fields with `{eq: null}` will return an error message in response payload. Before this change, the filter would have returned always an empty array. You can still retrieve fields with `null` value using `{eq: false}`
        
    -   **Color field**Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. You can use `{exists: false}` from now on.
        
    -   **DateTime field**Filtering fields with, for instance `{neq: "2020-04-09T00:00:00+02:00"}` will return *also* items with `null` values. Before this change, the filter would have returned only for `not null` values different from `2020-04-09T00:00:00+02:00`.
        
    -   **Date field**Filtering fields with, for instance `{neq: "2020-04-09"}` will return *also* items with `null` values. Before this change, the filter would have returned only for `not null` values different from `2020-04-09`.
        
    -   **Upload field**
        
        -   Filtering fields with `{eq: null}` now has the same effect of using `{exists: false}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with `{neq: null}` now has the same effect of using `{exists: true}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with, for instance `{neq: "123456"}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` uploads ids different from `123456`.
            
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
        -   **Important**: Filtering fields with `{in: []}` will return an empty collection. Before this change, the request would have returned all items.
            
        -   **Important**: Filtering fields with `{notIn: []}` will return all items. Before this change, the request would have returned an empty collection.
            
        -   **Important**: Filtering fields with, for instance `{notIn: ["123456"]}` will return all items having values different from `123456` **OR** equal to `null`. Before this change, the request would have returned only items having `not null`values different from `123456`.
            
    -   **Float fields**
        
        -   Filtering fields with, for instance, `{neq: "2.42"}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `2.42`.
            
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
    -   **Gallery**Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
        
    -   **Integer**
        
        -   Filtering fields with, for instance, `{neq: "5"}` now will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `5`.
            
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
    -   **JSON**Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
        
    -   **Position (geo points)**Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
        
    -   **Link**
        
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
        -   Filtering fields with `{eq: null}` now has the same effect of using `{exists: false}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with `{neq: null}` now has the same effect of using `{exists: true}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with, for instance, `{neq: "123456"}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `123456`.
            
        -   **Important**: Filtering fields with `{in: []}` will return an empty collection. Before this change, the request would have returned all items.
            
        -   **Important**: Filtering fields with `{notIn: []}` will return all items. Before this change, the request would have returned an empty collection.
            
        -   Filtering fields with, for instance `{notIn: ["123456"]}` will return all items having values different from `123456` **OR** equal to `null`. Before this change, the request would have returned only items having `not null`values different from `123456`.
            
    -   **Links**Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
        
    -   **Seo**Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
        
    -   **Slug**
        
        -   Filtering fields with, for instance, `{neq: "foobar"}` now will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with, for instance `{notIn: ["foobar"]}` will return all items having values different from `foobar` **OR** equal to `null`. Before this change, the request would have returned only items having `not null`values different from `foobar`.
            
    -   **String**
        
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
        -   Filtering fields with `{eq: null}` now has the same effect of using `{exists: false}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with `{neq: null}` now has the same effect of using `{exists: true}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with, for instance, `{neq: "foobar"}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with, for instance, `{notMatches: { pattern: "foobar"}}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with, for instance `{notIn: ["foobar"]}` will return all items having values different from `foobar` **OR** equal to `null`. Before this change, the request would have returned only items having `not null`values different from `foobar`.
            
    -   **Text**
        
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
        -   Filtering fields with, for instance, `{notMatches: { pattern: "foobar"}}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
    -   **Video**Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
        
-   **Changes to item metas**
    
    -   **ID**
        
        -   Filtering fields with `{eq: null}` will return an error message in response payload. Before this change, the filter would have returned an empty result.
            
        -   Filtering fields with `{neq: null}` will return an error message in response payload. Before this change, the filter would have returned an empty result.
            
    -   **Parent**
        
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
        -   Filtering fields with `{eq: null}` now has the same effect of using `{exists: false}`. Before this change, the filter would have returned always an empty array.
            
    -   **Position**Filtering fields with, for instance, `{neq: 3}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `3`.
        
    -   **Status**
        
        -   Filtering fields with `{eq: null}` will return an error message in response payload. Before this change, the filter would have returned an empty result.
            
        -   Filtering fields with `{neq: null}` will return an error message in response payload. Before this change, the filter would have returned an empty result.
            
-   **Changes to Upload fields**
    
    -   **Alt, Title**
        
        -   Added `exist` filter.
            
        -   Filtering fields with `{eq: null}` now has the same effect of using `{exists: false}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with `{neq: null}` now has the same effect of using `{exists: true}`. Before this change, the filter would have returned always an empty array.
            
        -   Filtering fields with, for instance, `{neq: "foobar"}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with, for instance, `{notMatches: { pattern: "foobar"}}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with, for instance `{notIn: ["foobar"]}` will return all items having values different from `foobar` **OR** equal to `null`. Before this change, the request would have returned only items having `not null` values different from `foobar`.
            
    -   **Author**
        
        -   Filtering fields with, for instance, `{notMatches: { pattern: "foobar"}}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
    -   **Copyright**
        
        -   Filtering fields with, for instance, `{notMatches: { pattern: "foobar"}}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
    -   **Format**Filtering fields with `{eq: null}`, `{neq: null}`, will now return an error message in response payload. Before this change, the request would have return an empy collection.
        
    -   **Height, Width**Filtering fields with, for instance, `{neq: "500"}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `500`.
        
    -   **ID**Filtering fields with `{eq: null}`, `{neq: null}`, will now return an error message in response payload. Before this change, the request would have return an empy collection.
        
    -   **InUse**Filtering fields with `{eq: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{eq: false}`. You can use `{eq: false}` from now on.
        
    -   **MimeType**Filtering fields with `{eq: null}`, `{neq: null}`, will now return an error message in response payload. Before this change, the request would have return an empy collection.
        
    -   **Notes**
        
        -   Filtering fields with, for instance, `{notMatches: { pattern: "foobar"}}` will return *also* items with `null` values. Before this change, the filter would have returned only for items with `not null` values different from `foobar`.
            
        -   Filtering fields with `{exists: null}` will return an error message in response payload. Before this change, the filter would have returned the same result as `{exists: false}`. Please, use `{exists: false}` instead.
            
    -   **Size**Filtering fields with `{eq: null}`, `{neq: null}`, will now return an error message in response payload. Before this change, the request would have return an empy collection.
        
    -   **SmartTags, Tags**Filtering fields with `{contains: null}`, will now return an error message in response payload. Before this change, the request would have return an empy collection.

---

# Content Management API — Content Management API Overview

Source [docs]: https://www.datocms.com/docs/content-management-api.md

This document is a detailed reference to DatoCMS's Content Management API.

The Content Management API (CMA) is used to manage the content of your DatoCMS projects. This includes creating, updating, deleting, and fetching content of your projects.

> [!NOTE] Content Management vs Content Delivery API
> If you want to programmatically create or update your schema/content, this is the API to use, while if you need to deliver content to your public-facing web or mobile projects, it is highly recommended that you use the GraphQL [Content Delivery API](/docs/content-delivery-api.md) instead, as it is under CDN and heavily optimized for fast response times!

### Core resources

The Content Management API features **40+ resources,** for a total of **150+ endpoints**. Check the following sections of this documentation for a complete reference. For each single resource you will find:

-   The resource object and its fields, attributes and relationships;
-   The allowed CRUD operations you can perform on the related endpoint with basic examples of the request/response format.
    

> [!WARNING] Some names might be different from what you expect!
> Due to historical reasons and backward compatibility, the name of some specific resources in the Content Management API is different from what you'll find in the interface of the product.
> 
> Specifically, Models are called **Item Types**, Records are called **Items**, and Assets are called **Uploads**.

### Base endpoint

All API requests must be made over HTTPS to the following base endpoint:

```plaintext
https://site-api.datocms.com
```

### Basic headers

The API follows the [JSON:API specification](https://jsonapi.org/) and provides an uniform and coherent way of working with every resource.

To perform an HTTP request with a body, you need to pass an `Accept: application/json` header:

Terminal window

```bash
curl \
  -H 'Accept: application/json' \
  -H 'X-Api-Version: 3' \
  https://site-api.datocms.com/site
```

To perform an HTTP request with a body, you need to pass a `Content-Type: application/vnd.api+json` header:

Terminal window

```bash
curl \
  -X PUT
  -H 'Accept: application/json' \
  -H 'X-Api-Version: 3' \
  -H 'Content-Type: application/vnd.api+json' \
  -d '{ ... }' \
  https://site-api.datocms.com/site
```

The header `Content-Type: application/json` is also valid, but not suggested.

### Authentication

To use the Content Management API, you will need to authenticate yourself with an API token. Read more about it in the [Authentication](/docs/content-management-api/authentication.md) section.

### Machine-readable API specification

We expose a machine-readable JSON schema that describes what resources are available via the API, what their URLs are, how they are represented and what operations they support. This schema follows the [JSON Schema format](http://json-schema.org/), combined with the draft [Validation](http://tools.ietf.org/html/draft-fge-json-schema-validation-00) and [Hypertext](http://tools.ietf.org/html/draft-luff-json-hyper-schema-00) extensions.

The latest version of the API schema will always be available at the following URL:

```plaintext
https://site-api.datocms.com/docs/site-api-hyperschema.json
```

---

# Content Management API — Using the JavaScript CMA client

Source [docs]: https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients.md

If you're familiar with Javascript/TypeScript, you can make use of our official client to perform requests to the Content Management API.

It offers a number of benefits over making raw requests yourself:

-   **The package is written in TypeScript**, so every method is fully typed and offers editor auto-completion and type checks;
-   Tedious tasks like [API rate limits retry](/docs/content-management-api/technical-limits.md), [asyncronous jobs management](/docs/content-management-api/async-jobs.md), [pagination](/docs/content-management-api/pagination.md) and [creation of new assets](/docs/content-management-api/resources/upload/create.md) are either **automatically managed for you, or greatly simplified** with simple methods that hide the inner complexities.
    

### How to install the client

DatoCMS provides three JavaScript client packages, each optimized for different runtime environments:

Terminal window

```bash
npm install @datocms/cma-client           # Generic/agnostic (recommended for most cases)
npm install @datocms/cma-client-node      # Node.js with filesystem access
npm install @datocms/cma-client-browser   # Browser-optimized
```

**`@datocms/cma-client`** **(generic/agnostic)** — The safest and most portable choice. Use it for:

-   Edge functions (Cloudflare Workers, Vercel Edge Functions, etc.)
-   Serverless functions (AWS Lambda, Netlify Functions, etc.)
    
-   Any environment where maximum compatibility is needed
    

**`@datocms/cma-client-node`** — Use this if you're in a Node.js environment to get the best experience. It provides specialized helper methods for [uploading files](/docs/content-management-api/resources/upload/create.md): `createFromLocalFile()` for local filesystem files and `createFromUrl()` for remote URLs. This gives you the most convenient API when working with Node.js and filesystem access.

**`@datocms/cma-client-browser`** — Use this if you're in a browser environment to get the best experience. It provides the [`createFromFileOrBlob()`](/docs/content-management-api/resources/upload/create.md) helper method for handling `File` and `Blob` objects from `<input type="file" />` elements, giving you the most convenient API when working with user file uploads.

If you don't need these specialized upload helpers, the generic `@datocms/cma-client` package is the best choice — it works in more environments and avoids potential compatibility issues.

### Initializing the client

You can use the `buildClient` function to initialize a new client.

```javascript
import { buildClient } from '@datocms/cma-client-node';

const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
```

##### Specifying a sandbox environment

By default, every API request you perform will point to the current primary environment, but if you want to make changes to a specific [sandbox environment](/docs/content-management-api/setting-the-environment.md), you can pass it in the initialization options:

```javascript
import { buildClient } from '@datocms/cma-client-node';

const client = buildClient({
  apiToken: process.env.DATOCMS_API_TOKEN,
  environment: 'my-sandbox-environment',
});
```

##### Logging request/responses

The client can output logs of the API request and responses it performs to help you debug issues with your code. You can choose different level of logging, depending on how much information you need:

```javascript
import { buildClient, LogLevel } from '@datocms/cma-client-node';

const client = buildClient({
  apiToken: process.env.DATOCMS_API_TOKEN,
  logLevel: LogLevel.BASIC,
});
```

The different levels of logging available are:

-   `LogLevel.NONE` (the default level): No output is generated;
-   `LogLevel.BASIC`: Logs HTTP requests (method, URL) and responses (status);
    
-   `LogLevel.BODY`: Logs HTTP requests (method, URL, body) and responses (status, body);
-   `LogLevel.BODY_AND_HEADERS`: Logs HTTP requests (method, URL, headers, body) and responses (status, headers, body).
    

### Entity collections and pagination

Take a look at the [Pagination](/docs/content-management-api/pagination.md) section to understand how pagination works, and which methods the JavaScript client provides to make your task easier.

### Raw vs Simplified endpoint methods

The client is organized by type of resource. For every resource, it offers a number of async methods to perform a CRUD request to a specific endpoint of the API:

```javascript
// Example: Item Type (Model)

await client.itemType.rawList(...);
await client.itemType.rawFind(...);
await client.itemType.rawCreate(...);
await client.itemType.rawUpdate(...);
await client.itemType.rawDestroy(...);
```

*"Why the* `*raw*` *prefix on all the methods?"* you might ask. Well, let's take a closer look at one specific method call — in this case, the update of an existing model.

As already covered in previous sections, the API follows the `JSON:API` convention, which requires a specific format for the payloads. Every request/response has a `data` attribute, which contains a number of [Resource Objects](https://jsonapi.org/format/#document-resource-objects), which in turn contain different of top-level members (`id`, `type`, `attributes`, `relationships`, `meta`, etc), each with their own semantic:

```javascript
const response = await client.itemTypes.rawUpdate('34532432', {
  data: {
    id: '34532432',
    type: 'item_type',
    attributes: {
      name: 'Article',
      api_key: 'article',
    },
    relationships: {
      title_field: { data: { id: '451235', type: 'field' } },
    },
  }
});

console.log(`Created model ${response.data.attributes.name}!`);
```

As you can see from the example above, it can become very verbose to write even simple code using this format! That's why the client also offers a "simplified" method for every endpoint — without the `raw` prefix — which greatly reduces the amount of boilerplate code required:

```javascript
const itemType = await client.itemTypes.update('34532432', {
  name: 'Article',
  api_key: 'article',
  title_field: { id: '451235', type: 'field' },
});

console.log(`Created model ${itemType.name}!`);
```

So the complete set of methods available for the Model resource is:

```javascript
// Example: Item Type (Model)

await client.itemType.list(...);
await client.itemType.rawList(...);

await client.itemType.find(...);
await client.itemType.rawFind(...);

await client.itemType.create(...);
await client.itemType.rawCreate(...);

await client.itemType.update(...);
await client.itemType.rawUpdate(...);

await client.itemType.destroy(...);
await client.itemType.rawDestroy(...);
```

In the next sections, you'll find a real-world usage example of the client for every endpoint offered by the API.

### Error management

In case an [API call fails](/docs/content-management-api/errors.md) with HTTP status code outside of the 2xx range, an `ApiError` exception will be raised by the client, containing all the details of the request/response.

```javascript
import { ApiError } from '@datocms/cma-client-node';

try {
  await client.itemType.create({
    name: 'Article',
    api_key: 'article',
  });
} catch(e) {
  if (e instanceof ApiError) {
    // Information about the failed request
    console.log(e.request.url);
    console.log(e.request.method);
    console.log(e.request.headers);
    console.log(e.request.body);

    // Information about the response
    console.log(e.response.status);
    console.log(e.response.statusText);
    console.log(e.response.headers);
    console.log(e.response.body);
  } else {
    throw e;
  }
}
```

The error object also includes a `.findError()` method that you can use to check if the response includes a particular error code:

```javascript
// finds in the array of api_error entities an error with code 'INVALID_FIELD',
// that in its details has the key 'field' set to 'api_key':
const errorEntity = e.findError('INVALID_FIELD', { field: 'api_key' });
```

### `SchemaRepository` utility for efficient schema access

When working with complex operations that require frequent access to schema information (models, fields, fieldsets, and plugins), the `SchemaRepository` utility provides an efficient caching layer to avoid redundant API calls.

```typescript
class SchemaRepository {
  constructor(client: GenericClient)

  // Item Type methods
  async getAllItemTypes(): Promise<ItemType[]>
  async getAllModels(): Promise<ItemType[]>
  async getAllBlockModels(): Promise<ItemType[]>
  async getItemTypeByApiKey(apiKey: string): Promise<ItemType>
  async getItemTypeById(id: string): Promise<ItemType>

  // Field methods
  async getItemTypeFields(itemType: ItemType): Promise<Field[]>
  async getItemTypeFieldsets(itemType: ItemType): Promise<Fieldset[]>

  // Plugin methods
  async getAllPlugins(): Promise<Plugin[]>
  async getPluginById(id: string): Promise<Plugin>
  async getPluginByPackageName(packageName: string): Promise<Plugin>

  // Raw variants (return full JSON:API response format)
  async getAllRawItemTypes(): Promise<RawItemType[]>
  async getRawItemTypeByApiKey(apiKey: string): Promise<RawItemType>
  // ... and more raw variants
}
```

###### **Purpose**

`SchemaRepository` is designed to solve performance problems when repeatedly fetching the same schema information during operations that traverse nested blocks, structured text, or modular content. It acts as an in-memory cache for schema entities.

Without `SchemaRepository`, a script processing fields containing nested blocks might make the same `client.itemTypes.list()` or `client.fields.list()` calls dozens of times: `SchemaRepository` ensures each unique schema request is made only once.

```typescript
import { SchemaRepository, mapBlocksInNonLocalizedFieldValue } from '@datocms/cma-client';

const schemaRepository = new SchemaRepository(client);

// These calls will hit the API and cache the results
const models = await schemaRepository.getAllModels();
const blogPost = await schemaRepository.getItemTypeByApiKey('blog_post');

// These subsequent calls will return cached results (no API calls)
const sameModels = await schemaRepository.getAllModels();
const sameBlogPost = await schemaRepository.getItemTypeByApiKey('blog_post');

// Pass the repository to utilities that need schema information
await mapBlocksInNonLocalizedFieldValue(record.content, 'rich_text', schemaRepository, (block, path) => {
  // The utility will use the cached schema data internally
});
```

###### What's it for

-   **Caching schema entities**: Automatically caches item types, fields, fieldsets, and plugins after the first API request
-   **Complex traversal operations**: Essential when using utilities like [`mapBlocksInNonLocalizedFieldValue()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#recursive-block-operations) that need to repeatedly lookup block models and fields
    
-   **Bulk operations**: Ideal for scripts that process multiple records of different types
-   **Read-heavy workflows**: Perfect for scenarios where you need to repeatedly access the same schema information
    

###### When NOT to use it

-   **Schema modification**: Do NOT use if your script modifies models, fields, fieldsets, or plugins, as the cache will become stale!
-   **Long-running applications**: The cache has no expiration mechanism!
    
-   **Concurrent schema changes**: No protection against cache inconsistency!
    

> [!PROTIP] Pro tip: Best practices
> Create one instance per script execution, not per operation, and make sure to use `SchemaRepository` consistently throughout your script for maximum cache efficiency!

### Ponyfilling `fetch()`

If your Javascript environment does not provide the [Fetch API interface](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) — for example, if you are using a version of Node lower than 18 — you will need to specify a ponyfill during client configuration:

```javascript
import { buildClient } from '@datocms/cma-client-node';
import { fetch } from '@whatwg-node/fetch';

const client = buildClient({ apiToken: '<YOUR_TOKEN>', fetchFn: fetch });
```

---

# Content Management API — API versioning

Source [docs]: https://www.datocms.com/docs/content-management-api/api-versioning.md

The latest version of the Content Management API is version 3. On every request you perform, you **MUST** specify the API version with the `X-Api-Version` header:

Terminal window

```bash
curl \
  -H 'Authorization: Bearer <YOUR-API-TOKEN>' \
  -H 'Accept: application/json' \
  -H 'X-Api-Version: 3' \
  https://site-api.datocms.com/site
```

## Breaking changes

The guarantee that the Content Management API offers is to **introduce breaking changes only when the API version changes**.

It is considered a "breaking change":

-   a change in path for an existing endpoint;
-   a change in the format of the request/response payload compared to what stated in this API reference;
    

We reserve the right to change the format of payloads **without changing API version** only when:

-   an attribute/relationship that previously was mandatory in a HTTP request becomes optional;
-   a new optional attribute/relationship is introduced in a HTTP request;
    
-   a new attribute/relationship is introduced in a HTTP response;
-   a synchronous endpoint becomes [asynchronous](/docs/content-management-api/async-jobs.md), but the job result has exactly the same signature as the old synchronous endpoint.
    

In other words, **no fields will ever be removed from the responses, but new ones might be added** if they do not break former behaviours.

---

# Content Management API — Authentication

Source [docs]: https://www.datocms.com/docs/content-management-api/authentication.md

In order to make any request to the Content Management API (CMA), you need to first obtain an API token. Enter your project administrative area (ie. `http://your-project.admin.datocms.com`) and go to the *Project Settings \> API Tokens* section:

(Video content)

Every project comes with a read-only API token by default. If you need to perform other types of requests — like writing or deleting content — you can create a custom API token with the appropriate permissions. You can use roles to define granular access levels and control exactly what each token can do.

Once you have the API Token, you need to pass it as an `Authorization` header in each HTTP request you perform:

Terminal window

```bash
curl \
  -H 'Authorization: Bearer <YOUR-API-TOKEN>' \
  -H 'Accept: application/json' \
  -H 'X-Api-Version: 3' \
  https://site-api.datocms.com/site
```

If you're using our [Javascript client,](/docs/content-management-api/using-the-nodejs-clients.md) you can pass the same API token as an option to the `buildClient` initialization function:

```javascript
import { buildClient } from '@datocms/cma-client-node';

const client = buildClient({
  apiToken: '<YOUR_TOKEN>',
});
```

---

# Content Management API — Environments

Source [docs]: https://www.datocms.com/docs/content-management-api/setting-the-environment.md

Every DatoCMS project has one [primary environment, and can have multiple sandbox environments](/docs/general-concepts/primary-and-sandbox-environments.md). Sandbox environments are very useful to test changes in your schema/content, without interfering with the regular flow of content editors.

By default, every API request you perform will point to the current primary environment, but if you want to make changes to a specific sandbox environment, you can add an `X-Environment` header.

Terminal window

```bash
curl \
  -H 'Authorization: Bearer <YOUR-API-TOKEN>' \
  -H 'Accept: application/json' \
  -H 'X-Api-Version: 3' \
  -H 'X-Environment: my-sandbox-environment' \
  https://site-api.datocms.com/site
```

If you're using our [Javascript client](/docs/content-management-api/using-the-nodejs-clients.md), you can pass the sandbox environment in the initialization options:

```javascript
import { buildClient } from '@datocms/cma-client-node';

const client = buildClient({
  apiToken: '<YOUR_TOKEN>',
  environment: 'my-sandbox-environment',
});
```

The legacy JS client has a similar option too:

```javascript
const { SiteClient } = require('datocms-client');

const client = new SiteClient('YOUR-API-TOKEN', {
  environment: 'my-sandbox-environment',
});
```

---

# Content Management API — Error codes & handling failures (CMA)

Source [docs]: https://www.datocms.com/docs/content-management-api/errors.md

### Content Management API Errors

CMA errors happen when your automation script, backend, or (rarely) frontend experiences a failure while talking to our REST Content Management API. This can happen for a variety of reasons, detailed below.

> [!NOTE] These errors are only for the REST Content Management API
> If you're looking for errors related to our GraphQL Content Delivery API, please instead see: [Error codes & handling failures (CDA)](/docs/content-delivery-api/errors.md)

## Non-200 HTTP Status Codes

When an Content Management API endpoint **fails for any reason,** it will return an **HTTP status code outside the** `**2xx**` **range**.

Parse its JSON response body and use the detailed error codes below to troubleshoot.

## CMA Error JSON Body Response Structure

The response body will include an array of **api\_error** entities that provide detailed information about the issue. Each error entity contains the following attributes:

-   **`code`**: A unique identifier for the specific error.
-   **`doc_url`**: A link to this documentation page for additional context.
    
-   **`details`**: Additional information describing the cause of the error.
-   **`transient`** *(optional)*: If set to `true`, this indicates the error is temporary. You can retry the request later as the issue may resolve itself.
    

As an example, this could be the response for a request that tries to [create a new model](/docs/content-management-api/resources/item-type/create.md), but the provided `api_key` is already used by another model:

```json
{
  "data": [
    {
      "id": "ce9dcb",
      "type": "api_error",
      "attributes": {
        "code": "INVALID_FIELD",
        "doc_url": "https://www.datocms.com/docs/content-management-api/errors#INVALID_FIELD",
        "details": {
          "field": "api_key",
          "code": "VALIDATION_UNIQUENESS",
          "record_type": "ItemType"
        }
      }
    }
  ]
}
```

## Error codes

###### `ACCOUNT_ALREADY_JOINED_SITE`

This error occurs when an attempt is made to invite a collaborator with an email that is already a member of the specified DatoCMS project. Ensure that the email provided for the user you are trying to invite is not already associated with an existing user in the project, or with the owner of the project itself.

###### `ALREADY_PRIMARY_ENVIRONMENT`

This error occurs when an attempt is made to promote an environment that is already set as the primary. Ensure that the environment you are trying to promote differs from the existing primary.

###### `BILLING_PROFILE_DEFAULTING`

This error occurs when attempting to perform an operation on a billing profile that has been cancelled. Ensure the billing profile is in an active state before retrying the request.

###### `CANNOT_CREATE_OR_DESTROY_SCHEMA_MENU_ITEM_LINKED_TO_ITEM_TYPE`

This error occurs when attempting to create or delete schema menu items that are linked to a specific model. Such entities are read-only and are automatically managed by DatoCMS. Only schema menu items not linked to an model can be created/modified/deleted.

###### `CANNOT_DESTROY_CURRENT_USER`

This error occurs when an API token attempts to delete itself. To resolve this issue, ensure that the request is made by a different access token than the one intended for deletion.

###### `CANNOT_DESTROY_FORKING_ORIGIN`

This error occurs when attempting to delete an environment that is currently serving as an origin for another environment that is still being created. To resolve this, ensure that no existing environment is being forked from the one you wish to delete, or wait for the creation processes to complete before retrying the deletion request.

###### `CANNOT_DESTROY_PRIMARY_ENVIRONMENT`

This error occurs when an attempt is made to delete the primary environment of a DatoCMS project. The primary environment serves as the main staging area for content, and deletion is not permitted to maintain data integrity. To resolve this, ensure you're targeting a non-primary environment for deletion.

###### `CANNOT_UPGRADE_MODERN_PLUGIN_INTO_LEGACY`

This error occurs when attempting to update an existing modern plugin by setting properties that only make sense for legacy DatoCMS plugins (i.e., `plugin_type`). While it's possible to convert a legacy plugin into a modern one, the opposite is not possible.

###### `CLASHING_FIELD_LABELS`

This error occurs when attempting to delete a fieldset, and the fieldset includes a field that carries a label already used by another field at the root level. To resolve this, make sure that all field labels within the fieldset are distinct.

###### `CONCURRENT_ENVIRONMENT_UPDATE`

This error happens when you try to modify an environment while another operation is also updating the same environment. To resolve this issue, ensure that no other requests are modifying the targeted environment at the same time, or implement logic to retry the request after a delay.

###### `CONCURRENT_ITEM_TYPE_UPDATE`

This error occurs when you try to modify a model or any entity connected to it while validation is in progress. To resolve this, please wait for the current validation to complete before trying your request again.

###### `CONCURRENT_ITEM_UPDATE`

This error occurs when an attempt is made to modify a record that is concurrently being updated by another API request. This typically happens when two API requests try to change the same record at the same time. To resolve this, ensure that you implement retry logic in your application, allowing it to gracefully handle such conflicts by retrying the request after a brief wait.

###### `CONCURRENT_ROLE_UPDATE`

This error occurs when an attempt is made to modify a role that is concurrently being updated by another API request. This typically happens when two API requests try to change the same role at the same time. To resolve this, ensure you implement retry logic in your application, allowing it to gracefully handle such conflicts by retrying the request after a brief wait.

###### `CONCURRENT_UPLOAD_UPDATE`

This error occurs when an attempt is made to modify an upload that is concurrently being updated by another API request. This typically happens when two API requests try to change the same content upload at the same time. To resolve this, ensure that you implement retry logic in your application, allowing it to gracefully handle such conflicts by retrying the request after a brief wait.

###### `DEACTIVATED_SITE`

This error happens when an API request tries to change content on a deactivated project. A deactivated project doesn't allow content modifications. To fix this, go to the DatoCMS dashboard and address any pending billing issues.

###### `DELETE_RESTRICTION`

This error occurs when an API request attempts to delete a resource, but there's a restriction or constraint that prevents the deletion from happening. Ensure that the content you are trying to delete is not being referenced or constrained by other linked entities. This error is not related to missing user permissions. Check your request for dependencies and remove or update those references before retrying the deletion.

###### `DESTINATION_USER_REQUIRED`

This error happens when you try to delete a collaborator or API token that created resources (records/uploads). Make sure the API request specifies a valid destination that will take ownership; otherwise, the operation cannot be completed.

###### `DESTRUCTIVE_REQUEST_BLOCKED`

This error occurs when a write request (anything other than `GET` or `HEAD`) is sent with the `X-Abort-If-Destructive-Request` header — typically by DatoCMS's official MCP server in read-only mode. To resolve it, use the unsafe script execution tools instead of the safe ones.

###### `DUPLICATE_POSITIONS_FOUND`

This error happens when an API request breaks position rules in a tree-like structure — i.e., fields/fieldsets/menu items/upload collections — often during reordering. To fix this, make sure all positions for entities are unique within their parent context to avoid duplicates. Check your payload before sending the request to prevent conflicts.

###### `DUPLICATE_SINGLETON`

This error happens when trying to create a record for a "Single instance" model, but a record of that type already exists in the project. If the model is set as "single instance", check that no other instance exists before performing the request.

###### `EMAIL_NOT_VERIFIED`

This error is returned when an action requires the account's email address to be verified, but the verification has not yet been completed. A verification email has been sent (or re-sent) to the address on file. Use the `email_verification_token_sent_at` field in the error details to know when the link was last dispatched, and prompt the user to check their inbox and click the verification link before retrying the operation.

###### `ENVIRONMENT_IN_READ_ONLY_MODE`

This error occurs when an attempt to modify content is made while the environment is temporarily in read-only mode due to pending operations (such as an environment fast-fork). To resolve it, ensure that the environment is in a writable state before executing any modification requests.

###### `ENVIRONMENT_NOT_READY`

This error occurs when a modification request is made to an environment that is not in a "ready" state. To resolve this, ensure that the environment you’re targeting has transitioned to "ready" status. You can check the current environment's status via the DatoCMS interface or API before making modification requests.

###### `EXCEPTION`

This error occurs when DatoCMS encounters an unhandled exception. Should you encounter this error, we kindly ask that you contact our support team.

###### `IMMUTABLE_UPLOAD_TRACKS_IN_SANDBOX_ENVIRONMENT`

This error occurs when attempting to modify upload tracks in a non-primary environment. Specifically, it's triggered if the API token's associated account is using a sandbox environment rather than a production environment, which restricts such modifications. To resolve this, ensure that your requests to create or destroy tracks are made in the primary environment.

###### `INCOMPATIBLE_WITH_UPLOAD_STORAGE_SETTINGS`

This error occurs when trying to alter the Asset CDN settings for a project that uses Enterprise-level, custom upload storage configurations. If you have custom upload storage, you must manage these settings within your architecture.

###### `INSUFFICIENT_PERMISSIONS`

This error occurs when the current API token lacks sufficient permissions to perform the requested action. To resolve this, ensure that the API token being used has the necessary permissions for the attempted operation.

###### `INVALID_ACCEPT_HEADER`

This error happens when the API request to modify content doesn't have a valid "Accept" header (`application/json`, `application/vnd.api+json`). Make sure your request includes an "Accept" header that matches the media types required by the API.

###### `INVALID_API_VERSION`

This error occurs when the `X-Api-Version` header in your request does not match any supported API version for the current DatoCMS project. Ensure that your API requests specify a valid version (e.g., `1`, `2`, or `3`).

###### `INVALID_ATTRIBUTES`

This error occurs when a request to modify content includes attributes that do not match the expected schema for the specified model. Ensure that only valid fields defined in your DatoCMS model are included in the request payload, and double-check for any typos or extraneous data that may have been added inadvertently.

###### `INVALID_AUTHORIZATION_HEADER`

This error occurs when the provided API `Authorization` header is invalid or absent during requests to modify content. Ensure that the API token used in the request is valid and properly formatted.

###### `INVALID_CONTENT_TYPE_HEADER`

This error occurs when a request to modify content is made without a valid `Content-Type` header. Specifically, it is triggered if the header does not indicate a JSON format, which is required for POST, PUT, or PATCH requests. To resolve this, ensure that your requests contain the `Content-Type` header with `application/json` or `application/vnd.api+json`.

###### `INVALID_DATE`

This error occurs when the API receives a date value that does not conform to the expected format, potentially due to an incorrect timezone or an invalid date string. To resolve it, ensure that date values are in ISO 8601 format.

###### `INVALID_DESTINATION_TYPE`

This error happens when an API request includes an unrecognized ownership type. To fix this, make sure the specified type is either `user`, `account`, `organization`, `sso_user`, or `access_token`.

###### `INVALID_DESTINATION_USER`

This error occurs when you attempt to delete a collaborator or API token while designating an invalid destination for ownership transfer (specifically, the user being deleted). Ensure that the API request indicates a valid destination for ownership; otherwise, the operation cannot be finalized.

###### `INVALID_DRAFT`

This error occurs when an API request attempts to publish a draft that fails validation checks. To resolve this issue, ensure that the record meets the content model's validation criteria before attempting the operation.

###### `INVALID_ENDPOINT`

This error occurs when an API request is made to an endpoint that does not exist or is not accessible by the current user. Check the URL for typos and confirm that the endpoint is valid for the intended resource type and user permissions to ensure proper access when modifying content.

###### `INVALID_ENTITY_ID`

This error occurs when an API request attempts to pass an invalid or improperly formatted ID. Specifically, the entity ID must be a Version 4 UUID formatted in URL-safe base64. Ensure that the ID provided in the request adheres to these specifications to resolve the issue.

###### `INVALID_ENVIRONMENT`

This error occurs when an API request attempts to access or modify content within an environment that does not exist or is not accessible by the current API token. Verify that the desired environment is accessible, activated, and ready. Check your authentication and environment identifiers in the request.

###### `INVALID_FIELD`

This error occurs when an API request to alter content fails validation, typically due to inconsistencies or absent fields in the payload. Please examine the specifics of the error you received to identify which field is causing the problem.

###### `INVALID_FILTER_FIELDS_PARAM`

This error occurs when the API receives invalid filtering parameters. Common triggers include using incorrect field names, unsupported operators, or failing to provide necessary conditions in your query. To resolve the issue, ensure that your filter parameters conform to the expected query structure, including valid field names and proper formatting.

###### `INVALID_FORMAT`

This error occurs when the payload of an API request to alter content (POST, PUT, DELETE) fails validation, typically due to an invalid format or absent fields in the payload. To resolve this, please examine the specifics of the error you received to identify which part of the payload is causing the problem.

###### `INVALID_JSON_BODY`

This error occurs when the API receives a request containing malformed JSON data. Common triggers include incorrect syntax, missing braces, or non-JSON compliant data types in the request body. Ensure that your JSON is correctly formatted to resolve this issue.

###### `INVALID_ORDERING`

This error occurs when the combination of ordering parameters provided in your API request is invalid. Common triggers include specifying both a meta ordering and a field ordering simultaneously, or attempting to set an ordering on "single instance" models. Review your request to ensure that the ordering parameters are correctly configured.

###### `INVALID_ORDERING_FOR_SINGLETON_ITEM_TYPE`

This error occurs when attempting to set an ordering field on a single instance model. Single instance models do not support custom ordering. To resolve this issue, ensure that the model is not marked as a single instance before attempting to modify ordering fields.

###### `INVALID_ORDERING_FOR_SORTABLE_ITEM_TYPE`

This error occurs when an attempt is made to set an ordering field for a model designated as sortable or tree-like. To resolve this, ensure that the model’s attributes do not include the sortable or tree flags before modifying the ordering. Check the model's configuration and adjust accordingly.

###### `INVALID_PARAMS`

This error occurs when the API request includes query string parameters that do not meet the required validation criteria. Review your request for completeness and compliance with the expected structure, and ensure that it adheres to all relevant constraints.

###### `INVALID_PARENT`

This error occurs when a request attempts to modify an entity by assigning a parent that creates a circular relationship or exceeds the allowable hierarchy depth. Ensure that the parent entity being assigned is not already a child in the current modification path, and that the nesting limit is respected.

###### `INVALID_PARENT_ID`

This error occurs when a request attempts to assign an invalid or nonexistent parent in a record under a tree-like collection. Ensure that the specified parent ID is a valid string referencing an existing record within the project and that it is not identical to the record's own ID, as this creates a circular reference.

###### `INVALID_PLUGIN_VERSION`

This error occurs when an API request attempts to update a plugin to a version that is either incorrect or does not exist in the npm registry, typically due to an invalid or malformed version string. Ensure that the given package version is valid and corresponds with available plugin versions in the npm registry to resolve this issue.

###### `INVALID_POSITION`

This error occurs when the API request includes an invalid type for the `position` attribute during a record modification, particularly for sortable or tree-structured models. To resolve it, ensure that the `position` field is an integer and meets the required conditions for the target model in your DatoCMS project.

###### `INVALID_RELATIONSHIP`

This error occurs when a requested operation violates relationship constraints between entities in DatoCMS. Specifically, it may be triggered if you attempt to associate incorrect entities in a relationship. To resolve this, ensure that relationships between your entities are correctly defined and compatible.

###### `INVALID_REQUEST`

This generic error occurs when an API request fails validation due to mismatched required properties in the request payload.

###### `INVALID_SEARCH_INDEX_ID`

This error occurs when attempting to search using a `filter[search_index_id]` parameter with a value that does not correspond to any existing search index in your project. The search index ID you provided is either incorrect, the search index may have been deleted, or it is disabled. Please verify that you are using a valid and enabled search index ID from your project's search indexes list.

###### `INVALID_SITE`

This error only occurs when an authentication method different from the API token is used in the request and signals that it's not possible to trace the request back to a particular DatoCMS project. This error should not happen if you're using API tokens as the authentication method.

###### `INVALID_SITE_EXPORT_SETTINGS`

This error occurs in the context of offline backups, a DatoCMS Enterprise feature. Common triggers include improper values in the backup settings or an invalid adapter type. To resolve this, ensure that all required fields are correctly populated and conform to the expected types.

###### `INVALID_TYPE`

This error occurs when attempting to create or update content using an model identifier that doesn't exist within your project's schema. To resolve, verify that the `item_type` relationship in your API request matches a valid model ID from your project's content model.

###### `INVALID_UPLOAD_STORAGE_SETTINGS`

This error occurs in the context of custom uploads storage (S3, GCP, etc.), a DatoCMS Enterprise feature. Common triggers include improper values in the custom uploads storage settings or an invalid adapter type. To resolve this, ensure that all required fields are correctly populated and conform to the expected types.

###### `ITEM_LOCKED`

This error occurs when attempting to modify a record that is currently locked for editing by another user. It typically arises if a different session holds a lock on the record, preventing concurrent modifications. To resolve this, ensure that the record is unlocked or wait for the user holding the lock to complete their changes before retrying the API request.

###### `ITEM_TYPE_CANNOT_BE_CHANGED`

This error occurs when an attempt is made to change the type of an existing record in DatoCMS. Typically, this validation error arises during an update operation where the model specified in the request does not match the current model stored in the system. To resolve this, ensure that the model remains consistent with the defined schema during updates.

###### `ITEM_TYPE_IS_SINGLETON`

This error occurs when attempting to modify or duplicate a record of a type designated as "single instance," which means only one record of that model is allowed. To resolve this, ensure you're not trying to create a second record for a model that is defined as "single instance" within your DatoCMS project.

###### `ITEM_TYPE_NOT_FOUND`

This error occurs when the specified model in your API request does not match any existing models within the current project. Check that the model ID or API key is correct and that the current user has access to the associated project.

###### `KEEP_URL_CONTENT_TYPE_CONFLICT`

This error occurs when attempting to replace an asset with the `keep_url` strategy using a file that has a different MIME type (Content-Type). Even if the file extensions match, the underlying content type must be identical to avoid Content-Type header inconsistencies and ensure proper browser handling.

To resolve this error, either:

-   Use the `create_new_url` strategy to generate a new URL for the new content type
-   Ensure the replacement file has the exact same MIME type as the original file

###### `KEEP_URL_FORMAT_CONFLICT`

This error occurs when attempting to replace an asset with the `keep_url` strategy using a file that has a different format/extension. You cannot replace a `.png` file with a `.jpg` file while keeping the same URL, as this would make the URL misleading about the actual file format and could cause browser compatibility and caching issues.

To resolve this error, either:

-   Use the `create_new_url` strategy to generate a new URL with the correct extension
-   Ensure the replacement file has the same format/extension as the original file

###### `KEEP_URL_STORAGE_NOT_SUPPORTED`

This error occurs when attempting to replace an asset with the `keep_url` strategy while using custom upload storage settings. The `keep_url` strategy is only supported when using DatoCMS's default storage configuration.

To resolve this error, either:

-   Use the `create_new_url` strategy to generate a new URL for the replaced asset
-   Switch to DatoCMS's default storage settings if you need to use the `keep_url` strategy

###### `KIND_CANNOT_BE_CHANGED`

This error occurs when an attempt is made to modify the "kind" of an existing schema menu item within DatoCMS. This validation ensures that the record retains its original structure, which is crucial for maintaining data integrity across the content management system. To resolve this, ensure that the "kind" attribute is not modified in your API request.

###### `MAINTENANCE_MODE`

This error occurs when the current site's primary environment is under maintenance, preventing any modification requests. To resolve this issue, confirm that maintenance mode is disabled for the project or coordinate with platform admins to schedule necessary updates outside maintenance periods.

###### `MISSING_FIELDS`

This error occurs when a request to create a record lacks some required fields. To resolve this, inspect the details of the error to understand which fields are missing, and ensure that your API request includes all mandatory fields.

###### `MISSING_LOCALES`

This error occurs when a request to create a record containing localized fields does not specify a value for any locale at all. To resolve this, ensure that you provide at least one value for one of the environment locales for each of the localized fields of the model.

###### `MISSING_QUERY_PARAMETER`

This error occurs when a Site Search does not include the required `filter[query]` query string parameter with the actual search term. To resolve this, ensure that the client request includes this parameter with a valid value.

###### `MODULAR_BLOCK_IN_USE`

This error occurs when an attempt is made to delete a block model that is currently in use by one or more structured text or modular content fields within the environment. To resolve this, ensure that the block is not referenced by any field before executing deletions.

###### `MUX_ERROR`

This error occurs when an operation regarding video uploads is attempted through the API, but Mux is unable to process it. To resolve this issue, please inspect the details of the error message provided for more specific information about what went wrong.

###### `NEW_PLUGIN_VERSION_IS_INCOMPATIBLE`

This error occurs when a request attempts to upgrade a legacy plugin with different settings for either the plugin type, field types in which it can operate, or the settings that the plugin offers. Review your API request payload for discrepancies to resolve the issue.

###### `NON_EDITABLE_ACCESS_TOKEN`

This error occurs when attempting to modify or delete a non-editable API token for the project — either the Full-access API token or the Read-only API token. To resolve this, ensure that you are not trying to modify or delete such tokens.

###### `NOT_A_VIDEO`

This error occurs when an API request attempts to add a track — either an additional audio track or a subtitle — to an upload that is not classified as a video. Ensure that the upload you're working with is a valid video file to resolve this issue.

###### `NOT_FOUND`

This error occurs when an API request attempts to access a resource that is not present in the system. Common triggers for this issue include specifying an invalid ID in your request. To resolve this, verify the entity ID and ensure that the API token has the necessary permissions to access it.

###### `NOT_ON_PER_SITE_PRICING`

This error occurs when attempting to read entities that are only available in a DatoCMS project under the legacy per-project pricing. To resolve it, ensure that the current account or organization that owns the project has subscribed to a per-project pricing plan.

###### `NO_PRIMARY_AUDIO_TRACK`

This error occurs when an attempt is made to generate automatic subtitles on uploads that lack a designated primary audio track. To resolve the issue, ensure that the upload associated with your request includes a valid primary audio track before proceeding with the operation.

###### `PLAN_UPGRADE_REQUIRED`

This error occurs when a request attempts to exceed the limits defined by the current subscription plan for workflows, upload sizes, or similar features. To resolve this, review your account's plan details and consider upgrading if you need access to additional resources or functionality.

###### `PLATFORM_SCHEDULED_MAINTENANCE`

This error arises when an API request is submitted during scheduled maintenance. During such maintenance, all DatoCMS projects become read-only. To address this issue, please check the status of the scheduled maintenance at [https://status.datocms.com](https://status.datocms.com/)

###### `PLUGIN_NOT_FOUND_IN_MARKETPLACE`

This error occurs when trying to turn a private plugin into a public one by setting `package_name` to a package that is not published in the DatoCMS Marketplace. Make sure the package name is correct and that the plugin has actually been published to the Marketplace.

###### `PRIMARY_ENVIRONMENT_SETTINGS_READ_ONLY`

This error occurs if the "Force the use of sandbox environments" setting is activated for a DatoCMS project and an attempt is made to change the primary environment settings – including changes to its content schema and role permission rules regarding the environment. To resolve this issue, either disable the "Force the use of sandbox environments" flag or ensure that your API request targets a sandbox environment.

###### `PUBLISHED_CHILDREN`

This error occurs when attempting to unpublish a record in a tree-structured collection that has one or more published child records. To resolve it, either ensure that all published children are unpublished before performing the unpublish action on the parent record, or pass the `recursive=true` query string parameter to the request.

###### `PUBLISHED_REFERENCES`

This error occurs when attempting to unpublish a record that is currently referenced by one or more published records. To resolve it, either change the `on_reference_unpublish_strategy` of the fields that are referencing the record to `delete_references` or `unpublish`, manually remove the references from the published records, or manually unpublish the records that reference this record as well. You can inspect the error message for more details on which specific records are causing the issue.

The error `details` contain:

-   `referencing`: the record that is actually blocked from being unpublished (a `{ item_id, item_type_id }` pair). This is not necessarily the record you requested to unpublish: unpublishing a record can cascade into other records (for example through fields whose `on_reference_unpublish_strategy` is `unpublish`), and the failure may surface deeper in that cascade.
-   `items`: the published records that reference `referencing` with a `fail` strategy.
-   `cascade_path_item_references`: the cascade path, as an array of `{ item_id, item_type_id }` pairs, from the record you requested to unpublish down to `referencing` (the last element always equals `referencing`). When no cascade took place, it contains a single element equal to `referencing`. Only top-level records appear in the chain — nested blocks are never included.

###### `RATE_LIMIT_EXCEEDED`

This error occurs when making a request to the API, but the number of requests exceeds the allowed limit within a specified time frame. To resolve the issue, ensure that your application throttles requests, waiting for the `X-RateLimit-Reset` period before retrying. Monitor your request volume and optimize where necessary to avoid triggering this limit.

###### `REQUIRED_BY_ASSOCIATION`

This error occurs when you attempt to delete a record that is currently referenced by another record. To resolve this, either change the `on_reference_delete_strategy` of the fields that are referencing the record to `delete_references`, or ensure that no record relies on this record before deleting it, and consider removing those references first.

###### `SERVICE_UNAVAILABLE`

This error occurs when our servers are temporarily unable to handle your request. This could be due to planned or unplanned maintenance, a system upgrade, or a server failure. These errors can also be returned during periods of high traffic. We suggest monitoring the API status at [https://status.datocms.com](https://status.datocms.com/) for ongoing issues that may affect service availability.

###### `SITE_NOT_READY`

This error occurs when an API request attempts to access or modify content within a project that is not yet accessible, as it's still being finalized. Verify that the desired project is accessible, activated, and ready.

###### `SSO_SETTINGS_REQUIRED`

This error occurs in the context of the Single-Sign On enterprise feature of DatoCMS, specifically when the API attempts to perform some operation but the SSO settings have not yet been configured. Ensure that Single Sign-On is enabled and its settings are properly set for the current project before making this request.

###### `STALE_ITEM_VERSION`

This error occurs when an attempt is made to update a record that has already been modified since it was last read. To resolve the issue, ensure that you're working with the latest record version by re-fetching the record before making updates, and verify that the current version matches the expected value in your request payload.

###### `TECHNICAL_LIMIT_REACHED`

This error occurs when you attempt to create new entities in your DatoCMS project but have exceeded allowed limits based on your current API token's subscription plan. It may happen if the content's byte size is too large, the number of blocks within a record exceeds the maximum, or if blocks are nested beyond permitted levels. To resolve it, inspect the error and find which limit is triggering the error, then check your subscription's limits and adjust your API request accordingly.

###### `TOO_MANY_OPERATIONS`

This error occurs when an API request exceeds the maximum allowed number of batch operations. To resolve it, ensure that the number of operations in your batch does not exceed the limit of 200.

###### `UNMANAGED_EDIT_CONFLICT`

This error typically arises when the current API token attempts to lock a record for editing, but another user has already locked it. To solve this issue, wait a few minutes and retry your request.

###### `UNPUBLISHED_LINK`

This error occurs when an attempt is made to publish a record that references other unpublished records. To resolve it, either change the `on_publish_with_unpublished_references_strategy` of the fields that are referencing the record to `publish_references`, manually remove the references from the record, or manually publish the referenced records as well. You can inspect the error message for more details on which specific records are causing the issue.

The error `details` contain:

-   `referenced_from`: the record that is actually blocked from being published (a `{ item_id, item_type_id }` pair). This is not necessarily the record you requested to publish: publishing a record can cascade into other records (for example through fields whose `on_publish_with_unpublished_references_strategy` is `publish_references`, or through unpublished tree parents), and the failure may surface deeper in that cascade.
-   `items`: the unpublished records that `referenced_from` links to with a `fail` strategy.
-   `cascade_path_item_references`: the cascade path, as an array of `{ item_id, item_type_id }` pairs, from the record you requested to publish down to `referenced_from` (the last element always equals `referenced_from`). When no cascade took place, it contains a single element equal to `referenced_from`. Only top-level records appear in the chain — nested blocks are represented by their top-level container.

###### `UNPUBLISHED_PARENT`

This error occurs when attempting to publish a record that has one or more parent records that are not yet published. Ensure that all parent records are successfully published before trying to publish the intended record. Check the details of the error and record's hierarchy to find the records that need to be published first.

###### `UNRESOLVABLE_SEARCH_INDEX`

This error occurs when attempting to search but no valid search index could be found. Either provide a valid `filter[search_index_id]` parameter to specify which search index to use, or ensure that at least one enabled search index exists in your project.

###### `UPLOAD_IS_CURRENTLY_IN_USE`

This error occurs when you attempt to delete an upload that is currently in use by one or more records in your DatoCMS project. To resolve this, first check which records are referencing the upload by looking at the error details. Ensure that all references are removed before re-attempting the deletion.

###### `UPLOAD_NOT_PASSING_FIELD_VALIDATIONS`

This error occurs when an upload is currently referenced by one or more records, and the change requested to the upload fails to meet the field validations in place for those records. To resolve this issue, you should review the validation rules for your content model.

###### `USED_AS_SLUG_SOURCE`

This error occurs when attempting to modify a field that a slug field depends on. To resolve it, identify the related slug in the details of the error, and either remove it or adjust its requirement before making the desired changes to the field.

###### `USE_SEARCH_INDEX_ID_INSTEAD_OF_BUILD_TRIGGER_ID`

This error occurs when attempting to search using `filter[build_trigger_id]` parameter on a build trigger that has multiple search indexes associated with it. Since the `build_trigger_id` parameter is ambiguous in this case, you must use the `filter[search_index_id]` parameter instead to explicitly specify which search index to query.

---

# Content Management API — Pagination

Source [docs]: https://www.datocms.com/docs/content-management-api/pagination.md

When it comes to obtaining complete lists of entities exposed by the API, a distinction needs to be made.

Some entities (i.e. models) can be retrieved all at once with a single API call, while others (i.e. records), are returned by the API in the form of pages. In the latter case, the response will contain the total number of resources in the `meta.total_count` property:

```json
{
  "data": [...],
  "meta": {
    "total_count": 140
  }
}
```

The pagination that the API offers is offset-based: this means that you can control the results that are returned with the parameters `page[limit]` and `page[offset]`:

-   `page[limit]` is the maximum number of entities to be returned
-   `page[offset]` is the (zero-based) offset of the first entity returned in the collection (always defaults to 0)
    

> [!NOTE] Page limit and maximum values vary by endpoint
> Both the default value of `page[limit]` and its maximum (that is, the maximum number of items that can be asked per page) vary depending on the specific endpoint. To obtain this information, refer to the specific documentation for the endpoint's `page` query parameter.

Setting a `page[limit]=5` and `page[offset]=5` will return entities 6 through 10:

Terminal window

```bash
curl \
  -H 'Accept: application/json' \
  -H 'Authentication: Bearer <YOUR-API-TOKEN>' \
  https://site-api.datocms.com/items?page[limit]=5&page[offset]=5
```

## Handling pagination with our JavaScript client

Our [JavaScript client](/docs/content-management-api/using-the-nodejs-clients.md) makes the `list()` method available for fetching collections of entities. As we have seen, depending on the endpoint, the result may contain all the entities in the collection, or just one page.

```javascript
// Returns all the models
const itemTypes = await client.itemTypes.list();

// Returns a single page of records
const items = await client.items.list();
```

In the case of a paginated endpoint, you can configure the pagination with `page.limit` and `page.offset`:

```javascript
// Returns the first 10 records
await client.items.list({ page: { limit: 10 } });

// Returns records 6 through 10
await client.items.list({ page: { limit: 5, offset: 5 } });
```

### Paged iterators

In the case of paginated entities, the client also provides the `listPagedIterator()` method, which allows for fetching all the pages of the collection in a simplified manner, without manually handling offset-based pagination.

You can use this method in an [async iteration statement](https://github.com/tc39/proposal-async-iteration#the-async-iteration-statement-for-await-of):

```javascript
// We'll be building up an array of all records using an AsyncIterator
const allRecords = [];

for await (
  const record of client.items.listPagedIterator(
    // You can define any query parameter that the endpoint permits,
    // except for page (refer to the following example for clarification)
    { filter: { type: "article" } }
  )
) {
  allRecords.push(record);
}

console.log(allRecords);
```

The method `listPagedIterator()` offers a few options to configure its behavior:

-   The `concurrency` option determines how many API calls can be performed in parallel (up to a maximum of 10). The default setting is 1, implying that the calls are made sequentially, not in parallel.
-   The `perPage` option specifies the size of the pages in the sub-requests that it will carry out in the background.
    

```javascript
for await (
  const record of client.items.listPagedIterator(
    // You can define any query parameter that the endpoint permits,
    // except for page
    { filter: { type: "article" } },
    // Pagination options
    { concurrency: 5, perPage: 100 },
  )
) {
  // ...
}
```

## Manually retrieving the total count of a query result

Normally, our [`listPagedIterator`](/docs/content-management-api/pagination.md#paged-iterators) handles pagination for you, but if you need to retrieve the total count of a query, you can use the `rawList()` command to access the response's `meta.total_count` property.

In this example, we query records (`items`) of a certain model type (`page`) using `rawList()` with a filter, and then access `meta.total_count` for the total.

```javascript
const records = await client.items.rawList({
        filter: {
            type: 'page' // API key (that you gave it) or ID (from its URL)
        },
        page: {
            limit: 0 // We don't need any actual records, just the meta
        }
    })

console.log(records.meta.total_count) // Returns `11`
```

---

# Content Management API — Asynchronous jobs

Source [docs]: https://www.datocms.com/docs/content-management-api/async-jobs.md

For some endpoints whose tasks are potentially time-consuming (e.g., [updating a Model](/docs/content-management-api/resources/item-type/update.md)), the API does not return a `200 OK` status code. Instead, a `202 Accepted` status code is returned, and an [asynchronous job](/docs/content-management-api/resources/job.md) starts in the background, which will complete shortly.

The payload of a `202 Accepted` response contains the ID of the asynchronous job that started:

```http
PUT https://site-api.datocms.com/item-types/:model_id_or_api_key HTTP/1.1
X-Api-Version: 3
Authorization: Bearer YOUR-API-TOKEN
Accept: application/json
Content-Type: application/json

{ ... }

HTTP/1.1 202 Accepted
Content-Type: application/json

{
  "data": {
    "type": "job",
    "id": "4235"
  }
}
```

To get the result of of the asynchronous job, you need to poll the [Job result](/docs/content-management-api/resources/job-result/self.md) endpoint. As long as the task is in progress, the endpoint will return a `404 Not found` status code. As soon as the job completes, the status will change to `200 OK`:

```http
GET https://site-api.datocms.com/job-results/:job_result_id HTTP/1.1
X-Api-Version: 3
Authorization: Bearer YOUR-API-TOKEN
Accept: application/json

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": {
    "type": "job_result",
    "id": "34",
    "attributes": {
      "status": 200,
      "payload": {
        "data": { ... }
      }
    }
  }
}
```

In the payload of the response you'll find both the status code and the payload of the original request you performed.

#### Important: all endpoints could return async jobs in the future!

If you are using our Content Management API directly, without any of our official clients, **you need to make sure to treat all endpoints as they might return an asynchronous job**.

This is a fundamental constraint of using our Content Management API. In the event that an endpoint needs to be optimized, we want to leave ourselves the ability to return an asynchronous job without having to release a new API version.

If you are implementing a custom client yourself, the easiest way to proceed is to **wrap every request you make to our API to a common logic** that checks if the response is a job, and if so, it polls for its result before returning anything to the caller.

> [!POSITIVE] Official clients and async jobs
> If you are using our [Javascript client](/docs/content-management-api/using-the-nodejs-clients.md), the whole async job concept is completely invisible. Everything is handled in the client itself, and the API methods return a `Promise` that resolves with the final result of the asynchronous job — or throw an exception if the job status code is different than `2xx`.

---

# Content Management API — CMA Technical Limits & Rate Limits

Source [docs]: https://www.datocms.com/docs/content-management-api/technical-limits.md

> [!NOTE] Content Management API (REST) Only
> These limits only apply to the Content Management API, our read/write REST API.
> 
> For other API limits, please see:
> 
> -   [CDA Technical Limits & Rate Limits](/docs/content-delivery-api/technical-limits.md)
>     
> -   [Real-time Updates API Limits & Pricing](/docs/real-time-updates-api/limits-and-pricing.md)

Our shared-service infrastructure is built to maintain steady performance for every customer, thanks to carefully set technical limits. If any API call or CMS action goes over these boundaries, it'll trigger an error message. Should your project require higher limits, [get in touch with us](https://www.datocms.com/support.md) to discuss further.

## CMA Technical Limits: Per-project and projectwide

Here are the technical limits currently in place for the CMA:

-   **Per-record limits (**[**read more**](/docs/content-modelling/record-block-limits-and-byte-size-limits.md)**):**
    
    -   **Maximum Record size**: 300 KB, including content in nested blocks (assets and linked records do not count toward the limit).
        
    -   **Number of blocks per record**: 500
        
    -   **Maximum depth for nested blocks**: 5 levels
        
    -   **Number of concurrent editors per record**: 1 (with presence indicator and record locking, [read more](/docs/general-concepts/collaboration-features.md))
        
-   **Project-wide limits:**
    
    -   **Assets upload**: Max size of 1 GB per asset
        
    -   **Plugin global user-defined settings**: Max size of 10KB per plugin ([read more](/docs/plugin-sdk/field-extensions.md#adding-user-defined-settings-into-the-mix))
        
    -   **Plugin field extension user-defined settings**: Max size of 10KB per field ([read more](/docs/plugin-sdk/field-extensions.md#adding-user-defined-settings-into-the-mix))
        

## CMA Rate Limits

The Content Management API is limited to **60 requests every 3 seconds**.

### **CMA Rate Limit Examples**

1.  You make **80 requests** at once and hit the limit immediately. The last 20 requests are rejected with a `429`. You should retry again after `x-ratelimit-reset: 3` seconds.
    
2.  You sustain **30 requests/second**. You will hit the limit in 2 seconds. The last 30 requests are rejected with a `429`. You should retry again after `x-ratelimit-reset: 1` second.
    
3.  You sustain **20 requests/second** and stay under the limit. Well done!
    

> [!POSITIVE] Official clients and rate limits
> Our [Javascript client](/docs/content-management-api/using-the-nodejs-clients.md) already manages rate limit errors for you with a retry mechanism! If it encounters a `429` status code, the promise won't be rejected. The client will repeat the requests until the API stops returning `429` status codes, and only then will the promise will be resolved with success.

> [!WARNING] 429 Status Responses in DatoCMS Shared Infrastructure
> Even when you are operating within your rate limits, there is a possibility of encountering a 429 status code in situations of high system load if your project is hosted on the DatoCMS shared infrastructure or medium-density infrastructure.
> 
> Nevertheless, it's essential to acknowledge that this occurrence is rare, and our official clients are equipped with an automatic retry mechanism to seamlessly handle such situations.

### **HTTP Headers for CMA Rate Limit**

Every CMA response will include these HTTP headers to help you stay within the rate limit:

-   `x-ratelimit-limit`: The number of requests you can make every 3 seconds (usually 60)
-   `x-ratelimit-remaining`: The number of *remaining* requests you can still make before the next refill
    

Exceeding the CMA rate limit will cause the HTTP status code to become `429 Too Many Requests`, and an additional header will be added:

-   `x-ratelimit-reset`: Number of seconds until the next refill (only appears when rate-limited)
    

## Reaching your plan monthly API calls limit

Every DatoCMS plan offers a number of API requests per month. What happens you exceed the included quota?

-   If your project is under a free plan, API responses will be temporarily disabled until the beginning of the following calendar month, unless you switch to a paid plan.
-   If your project is under a paid plan, you will pay an additional cost for the additional usage you made of the API.
    

For more details, check our [Plans, billing and pricing page](/docs/plans-pricing-and-billing.md).

---

# Content Management API — Record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item.md

DatoCMS stores the individual pieces of content you create from a model as records (for backwards compatibility the API calls these `item`). The shape of a record’s attributes depends on the fields defined by that record’s model — see the [Object payload](/docs/content-management-api/resources/item.md#object-payload) section for the full object payload documentation.

```json
// A simple record
{
  "id": "A4gkL_8pTZmcyJ-IlIEd2w",
  "type": "item",
  "attributes": {
    "title": "My Blog Post",
    "publication_date": "2024-01-15"
  },
  "relationships": {
    "item_type": {
      "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" }
    }
  }
}
```

> [!PROTIP] 📘 New to content modeling?
> Check out the [Content Modeling Guide](/docs/content-modelling.md) to understand how to design models, fields, and relationships before diving into API usage.

---

## Field types overview

###### Scalar fields

These store basic data types (ie. strings, numbers, booleans):

<details>
<summary>Single-line string</summary>

The field accepts `String` values or `null`.

</details>

<details>
<summary>Slug</summary>

The field accepts `String` values or `null`.

</details>

<details>
<summary>Multi-line text</summary>

The field accepts simple `String` values (can include newlines) or `null`

</details>

<details>
<summary>Boolean</summary>

The field accepts simple `Boolean` values or `null`.

</details>

<details>
<summary>Integer</summary>

The field accepts simple `Integer` values or `null`.

</details>

<details>
<summary>Float</summary>

The field accepts simple `Float` values or `null`.

</details>

<details>
<summary>Date</summary>

The field accepts `String` values in ISO 8601 date format (ie. `"2015-12-29"`) or `null`.

</details>

<details>
<summary>Date time</summary>

The field accepts `String` values in ISO 8601 date-time format (ie. `"2020-04-17T16:34:31.981+01:00"`) or `null`.

If you're on [legacy timezone management](https://www.datocms.com/product-updates/improved-timezone-management.md), remember that when sending an ISO8601 datetime you should keep in mind that the system will ignore any provided timezone, and will use the project's timezone instead.

</details>

<details>
<summary>JSON</summary>

The field accepts `String` values that are valid JSON or `null`.

**Note**: Must be a JSON-serialized string, not a JavaScript object!

</details>

###### Object Fields

These require structured objects:

<details>
<summary>Color</summary>

The field accepts an object with the following properties, or `null`:

| Property | Required | Type |
| --- | --- | --- |
| `red` | ✅ | `Integer` between 0 and 255 |
| `green` | ✅ | `Integer` between 0 and 255 |
| `blue` | ✅ | `Integer` between 0 and 255 |
| `alpha` | ✅ | `Integer` between 0 and 255 |

</details>

<details>
<summary>Location</summary>

The field accepts an object with the following properties, or `null`:

| Property | Required | Type |
| --- | --- | --- |
| `latitude` | ✅ | `Float` between -90.0 to 90 |
| `longitude` | ✅ | `Float` between -180.0 to 180 |

</details>

<details>
<summary>SEO</summary>

The field accepts an object with the following properties, or `null`:

| Property | Required | Type | Description |
| --- | --- | --- | --- |
| `title` |  | `String` | Title meta tag (max. 320 characters) |
| `description` |  | `String` | Description meta tag (max. 320 characters) |
| `image` |  | `Upload ID` | Asset to be used for social shares |
| `twitter_card` |  | `"summary"`, `"summary_large_image"` | Type of Twitter card to use |
| `no_index` |  | `Boolean` | Whether the noindex meta tag should be returned |

</details>

<details>
<summary>External video</summary>

The field accepts an object with the following properties, or `null`:

| Property | Required | Type | Description | Example |
| --- | --- | --- | --- | --- |
| `provider` | ✅ | `"youtube"`, `"vimeo"`, `"facebook"` | External video provider | `"youtube"` |
| `provider_uid` | ✅ | `String` | Unique identifier of the video within the provider | `"vUdGBEb1i9g"` |
| `url` | ✅ | `URL` | URL of the video | `"https://www.youtube.com/watch?v=qJhobECFQYk"` |
| `width` | ✅ | `Integer` | Video width | `459` |
| `height` | ✅ | `Integer` | Video height | `344` |
| `thumbnail_url` | ✅ | `URL` | URL for the video thumb | `"https://i.ytimg.com/vi/vUdGBEb1i9g/hqdefault.jpg"` |
| `title` | ✅ | `String` | Title of the video | `"Next.js Conf Booth Welcoming!"` |

</details>

###### Reference Fields

These point to other resources (either assets or other records):

<details>
<summary>Single-asset</summary>

The field accepts an object with the following properties, or `null`:

| Property | Required | Type | Description | Example |
| --- | --- | --- | --- | --- |
| `upload_id` | ✅ | `Upload ID` | ID of an asset | `"dhVR2HqgRVCTGFi0bWqLqA"` |
| `title` |  | `String` | Title for the asset, if you want to override the asset's default value (see Upload `default_field_metadata`) | `"From my trip to Italy"` |
| `alt` |  | `String` | Alternate text for the asset, if you want to override the asset's default value (see Upload `default_field_metadata`) | `"Florence skyline"` |
| `focal_point` |  | `{ x: Float, y: Float }`, `null` | Focal point for the asset, if you want to override the asset's default value (see Upload `default_field_metadata`). Values must be expressed as `Float` between 0 and 1. Focal point can only be specified for image assets. | `{ "x": 0.34, "y": 0.45 }` |
| `poster_time` |  | `Float`, `null` | Time (in seconds) into the video used to generate the thumbnail, if you want to override the asset's default value (see Upload `default_field_metadata`). Poster time can only be specified for video assets. | `12.5` |
| `custom_data` |  | `Record<String, String>` | An object containing custom keys that you can use on your frontend projects | `{ "watermark_image": "true" }` |

**API responses**: Always returns asset ID only (use separate asset API for details)

</details>

<details>
<summary>Asset gallery</summary>

This field accepts an `Array` of objects with the following properties, or `null`:

| Property | Required | Type | Description | Example |
| --- | --- | --- | --- | --- |
| `upload_id` | ✅ | `Upload ID` | ID of an asset | `"dhVR2HqgRVCTGFi0bWqLqA"` |
| `title` |  | `String` | Title for the asset, if you want to override the asset's default value (see Upload `default_field_metadata`) | `"Gallery Image Title"` |
| `alt` |  | `String` | Alternate text for the asset, if you want to override the asset's default value (see Upload `default_field_metadata`) | `"Gallery image description"` |
| `focal_point` |  | `{ x: Float, y: Float }`, `null` | Focal point for the asset, if you want to override the asset's default value (see Upload `default_field_metadata`). Values must be expressed as `Float` between 0 and 1. Focal point can only be specified for image assets. | `{ "x": 0.34, "y": 0.45 }` |
| `poster_time` |  | `Float`, `null` | Time (in seconds) into the video used to generate the thumbnail, if you want to override the asset's default value (see Upload `default_field_metadata`). Poster time can only be specified for video assets. | `12.5` |
| `custom_data` |  | `Record<String, String>` | An object containing custom keys that you can use on your frontend projects | `{ "watermark_image": "true" }` |

**API responses**: Always returns array of asset IDs only

</details>

<details>
<summary>Single link</summary>

This field accepts a `String` representing the ID of the linked record, or `null`. See [Link Fields Guide](/docs/content-modelling/links.md) for relationship modeling concepts.

**API responses**: Always returns record ID only

</details>

<details>
<summary>Multiple links</summary>

This field accepts an `Array<String>` representing the IDs of the linked records, or `null`. See [Link Fields Guide](/docs/content-modelling/links.md) for relationship modeling concepts.

**API responses**: Always returns array of record IDs only

</details>

###### Block Fields

These are special fields that contain **blocks within records**:

| Field Type | What it contains |
| --- | --- |
| **Modular content** | An array of blocks, perfect for building dynamic page sections |
| **Single block** | A single block instance or `null` |
| **Structured text** | A rich text document that can have blocks embedded within the flow of content ([DAST format](/docs/structured-text/dast.md)) |

Blocks are **records within records** - they're separate items that live inside fields of other records.

> [!PROTIP] 📚 Content Modeling Context
> To understand when and how to design blocks vs models, see [Blocks Guide](/docs/content-modelling/blocks.md). For field-specific concepts, see [Modular Content](/docs/content-modelling/modular-content.md) and [Structured Text](/docs/content-modelling/structured-text.md).

Blocks inside those fields are unique because they can be represented in two different ways depending on the context: as a lightweight reference (an ID) or as a full content object. Understanding this duality is key to working with them effectively:

-   **Block ID (Lightweight Reference)**: A simple `String` that uniquely identifies the block (ie. `"dhVR2HqgRVCTGFi_0bWqLqA"`). This is useful when you only need to know *which* block is there, not what's inside it.
-   **Block Object (Full Content)**: The complete record object for the block, containing its own `id`, `type`, `attributes`, and `relationships`. This is used when you need to read or modify the block's actual content.
    
    ```json
    {
      "id": "dhVR2HqgRVCTGFi_0bWqLqA",
      "type": "item",
      "attributes": {
        "title": "Block Title",
        "content": "Block content..."
      },
      "relationships": {
        "item_type": {
          "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" }
        }
      }
    }
    ```
    

<details>
<summary>Modular Content</summary>

A Modular Content field holds an array of blocks.

**As an array of IDs:**

```json
{
  "content_blocks": [
    "dhVR2HqgRVCTGFi_0bWqLqA",
    "kL9mN3pQrStUvWxYzAbCdE"
  ]
}
```

**As an array of full objects:**

```json
{
  "content_blocks": [
    {
      "id": "dhVR2HqgRVCTGFi_0bWqLqA",
      "type": "item",
      "attributes": { "title": "Hero Section", "content": "Welcome to our site" },
      "relationships": { "item_type": { "data": { "id": "...", "type": "item_type" } } }
    },
    {
      "id": "kL9mN3pQrStUvWxYzAbCdE",
      "type": "item",
      "attributes": { "title": "Image Gallery", "images": [...] },
      "relationships": { "item_type": { "data": { "id": "...", "type": "item_type" } } }
    }
  ]
}
```

</details>

<details>
<summary>Single Block</summary>

A Single Block field holds exactly one block, or `null`.

**As an ID:**

```json
{
  "featured_block": "dhVR2HqgRVCTGFi_0bWqLqA"
}
```

**As an full object:**

```json
{
  "featured_block": {
    "id": "dhVR2HqgRVCTGFi_0bWqLqA",
    "type": "item",
    "attributes": { "title": "Featured Content", "summary": "A summary..." },
    "relationships": { "item_type": { "data": { "id": "...", "type": "item_type" } } }
  }
}
```

</details>

<details>
<summary>Structured Text</summary>

A Structured Text field can contain blocks within its document structure ([DAST format](/docs/structured-text/dast.md)). The item property of a `block` or `inlineBlock` node will hold either the ID or the full object.

**With block IDs:**

```json
{
  "rich_text_content": {
    "schema": "dast",
    "document": {
      "type": "root",
      "children": [
        {
          "type": "paragraph",
          "children": [{ "type": "span", "value": "Text before block." }]
        },
        {
          "type": "block",
          "item": "dhVR2HqgRVCTGFi_0bWqLqA"
        }
      ]
    }
  }
}
```

**With full objects:**

```json
{
  "rich_text_content": {
    "schema": "dast",
    "document": {
      "type": "root",
      "children": [
        {
        "type": "paragraph",
          "children": [{ "type": "span", "value": "Text before block." }]
        },
        {
          "type": "block",
          "item": {
            "id": "dhVR2HqgRVCTGFi_0bWqLqA",
            "type": "item",
            "attributes": { "title": "Embedded Block", "content": "..." },
            "relationships": { "item_type": { "data": { "id": "...", "type": "item_type" } } }
          }
        }
      ]
    }
  }
}
```

</details>

---

## API response modes: Regular vs. Nested

When fetching record data, the API gives you control over how block fields are represented in the response. These two modes, **Regular** and **Nested**, are available on the following endpoints:

-   [Retrieve a single record (`GET /items/:id`)](/docs/content-management-api/resources/item/self.md)
-   [Retrieve multiple records (`GET /items`)](/docs/content-management-api/resources/item/instances.md)
-   [Retrieve records referenced by a record (`GET /items/:id/references`)](/docs/content-management-api/resources/item/references.md)
-   [Retrieve records linked to an asset (`GET /upload/:id/references`)](/docs/content-management-api/resources/upload/references.md)

###### Regular mode (default)

By default, the API returns block fields as IDs only. This is efficient and fast, making it ideal for listings or when you don't need the blocks' content immediately.

```json
GET /items/A4gkL_8pTZmcyJ-IlIEd2w

{
  "id": "A4gkL_8pTZmcyJ-IlIEd2w",
  "type": "item",
  "attributes": {
    "title": "My Blog Post",
    "content_blocks": ["dhVR2HqgRVCTGFi_0bWqLqA", "kL9mN3pQrStUvWxYzAbCdE"],
    "featured_block": "nZ8xY2vWqTuJkL3mNcBeFg"
  }
}
```

###### Nested mode (`?nested=true`)

The same endpoint, when passing the `?nested=true` option, returns **block fields as full objects**. This is essential when you need to display or edit the content within the blocks.

```json
GET /items/A4gkL_8pTZmcyJ-IlIEd2w?nested=true

{
  "id": "A4gkL_8pTZmcyJ-IlIEd2w",
  "type": "item",
  "attributes": {
    "title": "My Blog Post",
    "content_blocks": [
      {
        "id": "dhVR2HqgRVCTGFi_0bWqLqA",
        "type": "item",
        "attributes": { "title": "Hero Section", "content": "Welcome to our site" },
        "relationships": { ... }
      },
      {
        "id": "kL9mN3pQrStUvWxYzAbCdE",
        "type": "item",
        "attributes": { "title": "Image Gallery", "images": [...] },
        "relationships": { ... }
      }
    ],
    "featured_block": {
      "id": "nZ8xY2vWqTuJkL3mNcBeFg",
      "type": "item",
      "attributes": { ... },
      "relationships": { ... }
    }
  }
}
```

> [!WARNING] Block Fields vs. Other Reference Fields
> Block fields are the **only** field type that change representation between modes! Asset and link fields always return IDs. To get full details for assets or linked records, you need to make separate API calls using their IDs.

###### When to use each mode?

| Use "Regular Mode" when... | Use "Nested Mode" when... |
| --- | --- |
| Listing many records or building navigation. | Displaying or editing block content, as it provides the actual content needed. |
| You only need to know which blocks exist. | You need to read the actual block content for display or updates. |
| Building navigation | Preparing to update blocks |
| Performance is critical; it's faster because it returns smaller responses (block IDs instead of full content). | You are building content editing interfaces where usability is more important than raw speed. |

---

## Creating and updating blocks

Working with blocks follows one fundamental constraint:

**You cannot create, edit, or delete blocks directly. You must always update the parent record that contains them.**

This ensures data integrity. To create/modify blocks, you send a payload to the parent record's endpoint, using a mix of Block IDs and Block Objects to describe the desired changes.

###### Key rules for block operations

1.  **To create a new block**: Provide the **full object**, including `type`, `attributes`, and the `relationships.item_type` which specifies the Block Model being used.
2.  **To update an existing block**: Provide the **full object**, including its `id` and the changed `attributes`. You only need to include the specific attributes that you want to change - unchanged attributes will be preserved. You don't need to specify `relationships.item_type`.
3.  **To keep an existing block unchanged**: Simply provide its **Block ID** string. This is the most efficient way to handle unchanged blocks.
4.  **To delete a block**: Omit it from the payload. For a Modular Content array, remove its ID. For a Single Block field, set the value to `null`.
5.  **To reorder blocks** (in Modular Content): Send an array of Block IDs in the new desired order.

> [!PROTIP] 🆔 Custom block IDs
> Any new block payload (on create or update) may optionally include an `id` (an RFC 4122 v4 UUID expressed in URL-safe base64). If provided, that value becomes the new block's ID; otherwise the server generates one. On **update**, the `id` is interpreted as follows: if it matches a block already in the parent record, that block is updated; otherwise — provided the `id` is a valid v4-base64 UUID and not yet used by any record in the environment — a new block is created with that ID. An `id` that resolves to a record outside the parent, or is not a valid v4-base64 UUID, is rejected.

The following examples show how to apply these rules.

<details>
<summary>Working with Modular Content Fields</summary>

**Current state** (from a regular API response):

```json
{
  "content_blocks": ["dhVR2HqgRVCTGFi_0bWqLqA", "kL9mN3pQrStUvWxYzAbCdE", "fG8hI1jKlMnOpQrStUvWxY"]
}
```

**To update the second block and reorder the others:**

```json
{
  "content_blocks": [
    "fG8hI1jKlMnOpQrStUvWxY", // Reordered: kept as ID
    {
      "id": "kL9mN3pQrStUvWxYzAbCdE", // Updated: sent as object
      "type": "item",
      "attributes": { "title": "Updated Title" }
    },
    "dhVR2HqgRVCTGFi_0bWqLqA" // Reordered: kept as ID
  ]
}
```

**To add a new block at the end and remove the first block:**

```json
{
  "content_blocks": [
    "kL9mN3pQrStUvWxYzAbCdE", // Kept as ID
    "fG8hI1jKlMnOpQrStUvWxY", // Kept as ID
    {
      "type": "item", // New block: sent as object with relationships
      "attributes": { "title": "A Brand New Block" },
      "relationships": {
        "item_type": {
          "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" }
        }
      }
    }
  ]
}
```

</details>

<details>
<summary>Working with Single Block Fields</summary>

**Current state** (from a regular API response):

```json
{
  "hero_block": "dhVR2HqgRVCTGFi_0bWqLqA"
}
```

**To update the block's content:**

```json
{
  "hero_block": {
    "id": "dhVR2HqgRVCTGFi_0bWqLqA",
    "type": "item",
    "attributes": { "title": "Updated Hero Title" }
  }
}
```

**To replace it with a new block:**

```json
{
  "hero_block": {
    "type": "item",
    "attributes": { "title": "New Hero Block" },
    "relationships": {
      "item_type": {
        "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" }
      }
    }
  }
}
```

**To remove (delete) the block:**

```json
{
  "hero_block": null
}
```

</details>

<details>
<summary>Working with Structured Text Fields</summary>

Updating blocks within Structured Text follows the same pattern: you replace the `item`'s ID with a full object for the block you want to change.

**Current state** (from a regular API response):

```json
{
  "rich_content": {
    "schema": "dast",
    "document": {
      "type": "root",
      "children": [
        { "type": "block", "item": "dhVR2HqgRVCTGFi_0bWqLqA" },
        { "type": "paragraph", "children": [{ "type": "span", "value": "Some text." }] }
      ]
    }
  }
}
```

**To update the block's content:**

```json
{
  "rich_content": {
    "schema": "dast",
    "document": {
      "type": "root",
      "children": [
        {
          "type": "block",
          "item": {
            "id": "dhVR2HqgRVCTGFi_0bWqLqA", // The block to update
            "type": "item",
            "attributes": { "title": "Updated DAST Block Title" }
          }
        },
        { "type": "paragraph", "children": [{ "type": "span", "value": "Some text." }] }
      ]
    }
  }
}
```

</details>

###### Deeply-nested blocks

Blocks can contain other blocks, creating hierarchies multiple levels deep. **The same principles apply recursively.** When you fetch a record with `?nested=true`, the API will expand nested blocks at all levels.

When updating, you are always sending a payload to the top-level parent record, but you can specify changes to deeply nested blocks using the same ID vs. object rules.

<details>
<summary>Example: Updating a nested block</summary>

Imagine a "Wrapper" block that contains a Modular Content field with "Child" blocks inside it. To update "Child Block 1" while leaving "Child Block 2" untouched:

```json
// This payload is sent to the top-level record containing the "Parent Block"
{
  "wrapper_block": {
    "id": "dhVR2HqgRVCTGFi_0bWqLqA", // ID of the parent block being updated
    "type": "item",
    "attributes": {
      "nested_content": [
        {
          "id": "kL9mN3pQrStUvWxYzAbCdE", // ID of the nested block being updated
          "type": "item",
          "attributes": { "title": "Updated Child Block 1" }
        },
        "fG8hI1jKlMnOpQrStUvWxY" // Unchanged nested block, sent as ID
      ],
      // You can skip any attribute that does not need to change
    }
  }
}
```

</details>

---

## Localization

Localization allows you to store different versions of your content for different languages or regions. When you mark a field as "localizable" in your model, its structure in the API changes to accommodate multiple values.

The fundamental change is that the field's value is no longer a single piece of data but an **object keyed by locale codes**.

For example, a simple non-localized `title` field looks like this:

```json
{
  "title": "Hello World"
}
```

When localized, it becomes an object containing a value for each configured locale:

```json
{
  "title": {
    "en": "Hello World",
    "it": "Ciao Mondo",
    "fr": "Bonjour le Monde"
  }
}
```

This principle applies to **every type of field**, from simple strings to **Modular Content**, **Single Block**, and **Structured Text** fields. For instance, a localized Modular Content field will contain a separate array of blocks for each language. This powerful feature allows you to have completely different block structures for each locale.

<details>
<summary>Example: Localized Modular Content field</summary>

In a `regular` API response, you would see different arrays of block IDs for each locale.

```json
{
  "content_blocks": {
    "en": ["dhVR2HqgRVCTGFi0bWqLqA", "kL9mN3pQrStUvWxYzAbCdE"],
    "it": ["fG8hI1jKlMnOpQrStUvWxY", "dhVR2HqgRVCTGFi0bWqLqA"]
  }
}
```

</details>

<details>
<summary>Example: Localized Single Block field</summary>

A different block can be assigned to each locale.

```json
{
  "hero_block": {
    "en": "dhVR2HqgRVCTGFi0bWqLqA",
    "it": "kL9mN3pQrStUvWxYzAbCdE"
  }
}
```

</details>

<details>
<summary>Example: Localized Structured Text field</summary>

The entire DAST document is localized, allowing for different text and different embedded blocks per locale.

```json
{
  "rich_content": {
    "en": {
      "schema": "dast",
      "document": {
        "type": "root",
        "children": [
          {
            "type": "paragraph",
            "children": [
              {
                "type": "span",
                "value": "Welcome to our product showcase. Here's what we're featuring today:"
              }
            ]
          },
          { "type": "block", "item": "dhVR2HqgRVCTGFi0bWqLqA" }
        ]
      }
    },
    "it": {
      "schema": "dast",
      "document": {
        "type": "root",
        "children": [
          {
            "type": "paragraph",
            "children": [
              {
                "type": "span",
                "value": "Benvenuti nella nostra vetrina prodotti. Ecco cosa presentiamo oggi:"
              }
            ]
          },
          { "type": "block", "item": "kL9mN3pQrStUvWxYzAbCdE" }
        ]
      }
    }
  }
}
```

</details>

When reading or writing localized content, there are a few key rules to follow to ensure data integrity.

###### Locale consistency

Within a single record, all localized fields must have a consistent set of locales. You cannot have a `title` with English and Italian, and a `description` with English and French in the same record.

```json
// ❌ This will FAIL due to inconsistent locales ("it" vs "fr")
{
  "title": { "en": "Title", "it": "Titolo" },
  "description": { "en": "Description", "fr": "Description" }
}

// ✅ This is VALID because locales are consistent across all fields
{
  "title": { "en": "Title", "it": "Titolo" },
  "description": { "en": "Description", "it": "Descrizione" }
}
```

###### Models enforcing all locales

You can configure a model to require every project locale to be present for its localized fields using the [`all_locales_required`](/docs/content-management-api/resources/item-type.md#object-payload) attribute.

When this setting is enabled, records **must include a key for every defined locale** within each localized field. The value for a locale can be `null`, but the key itself is mandatory.

```json
// ❌ FAILS: The "it" locale is missing.
{
  "title": { "en": "Title" }
}

// ✅ VALID: All required locale keys ("en", "it") are present.
{
  "title": { "en": "Title", "it": "Titolo" }
}

// ✅ ALSO VALID: The "it" key is present, even with a `null` value.
{
  "title": { "en": "Title", "it": null }
}
```

---

## Type-safe development with TypeScript

Since DatoCMS records don't have a predetermined structure, the JavaScript client cannot provide strict TypeScript types out of the box:

```typescript
import { buildClient } from "@datocms/cma-client-node";

const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });
const record = await client.items.find('dhVR2HqgRVCTGFi0bWqLqA');

record.accent_color; // -> TypeScript type: unknown
```

To get **full type-safety plus auto-completions and type hints in your code editor**, you can leverage the DatoCMS CLI to automatically generate TypeScript types based on your specific project schema.

###### Generating types from your schema

After [installing and configuring the CLI](/docs/scripting-migrations/installing-the-cli.md), you can use the `schema:generate` command to generate a comprehensive TypeScript definition file describing your DatoCMS project structure (models and blocks):

Terminal window

```bash
$ npx datocms schema:generate schema.ts
```

The output describes your DatoCMS project structure (models and blocks) and emits, for each one, both an `ItemTypeDefinition` **type** and a runtime **constant** with `ID` / `REF` properties:

```typescript
// schema.ts — generated by `npx datocms schema:generate`.
//
// ⚠️ Do not hand-write or hand-edit these types in your own code. The
// generator is authoritative: re-run it whenever the schema changes, and
// import from the generated file. Redeclaring ItemTypeDefinition<...>
// inline duplicates the schema and drifts silently the moment a field
// is added, renamed, or removed.

import type { ItemTypeDefinition } from '@datocms/cma-client';

type EnvironmentSettings = { locales: 'en' | 'it' };

export type Article = ItemTypeDefinition<
  EnvironmentSettings,
  '76hhD-LaS5CM3NPJw0991w', // ID of the Article model
  {
    name: { type: 'string' };
    slug: { type: 'slug' };
    accent_color: { type: 'color' };
    sections: { type: 'rich_text'; blocks: ArticleSection };
  }
>;
export const Article = {
  ID: '76hhD-LaS5CM3NPJw0991w',
  REF: { type: 'item_type', id: '76hhD-LaS5CM3NPJw0991w' },
} as const;
```

An `ItemTypeDefinition` is a minimal type blueprint for your API payloads. It only includes what's needed for typed API calls: field names, their data types, and any allowed block types. It intentionally omits details like validation rules or default values, as they don't affect the shape of the data sent to or from the API.

> [!WARNING] Practical, not perfect
> These types are designed for a practical developer experience, not perfect precision. In other words, you might still encounter API errors even if TypeScript gives you the green light. The types ensure the structure of a request is valid, but not necessarily the values within it (e.g., a string that's too long).

> [!PROTIP] Other ways to generate types
> Use `--item-types=product,article` to scope generation to specific models/blocks, or include the same definitions inline in a [migration script](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md#option-1-write-a-migration-script-manually) via `npx datocms migrations:new 'tweak articles' --schema=article,author` (or `--schema=all`).

###### Using markers in API calls

Each generated `ItemTypeDefinition` (e.g. `Article`) acts as a **marker**: a branded type the client uses to infer the right request and response shapes when you pass it as a generic. By convention, import the generated file as a `Schema` namespace so each marker reads as `Schema.Article`, `Schema.ArticleSection`, etc.:

```typescript
import * as Schema from './schema';
```

Use a marker as a **type** when calling generic methods like `client.items.create<Schema.Article>(...)`, and as a **value** to reference the model's ID (`Schema.Article.ID`) or build an `item_type` relationship (`Schema.Article.REF`).

Markers can be used as generics in all API calls related to records to get a fully typed interface:

```typescript
// Fully typed record retrieval
const record = await client.items.find<Schema.Article>('AZUeMuPySxuJCJ8ibEVE7w');
record.accent_color; // -> { red; green; blue; alpha } (properly typed!)

// Type-safe record creation
const record = await client.items.create<Schema.Article>({
  item_type: Schema.Article.REF,
  accent_color: '#FF0000', // ✅ TypeScript catches the wrong format!
});
```

###### Extracting a concrete field type

When you need the actual TypeScript type of a field — to annotate a helper, an intermediate variable, or a function parameter — reach for one of the `FieldValue*` helpers. A marker can't be indexed directly: its top-level keys are blueprint metadata, not field API keys.

```typescript
// ❌ TypeScript error — Schema.Article describes the model, not its payload
type Sections = Schema.Article['sections'];
```

Which helper you pick depends on which side of the wire you're on:

| Helper | Resolves to the field as it appears in… |
| --- | --- |
| `FieldValueInRequest<T, 'field_key'>` | a `client.items.create` / `client.items.update` payload |
| `FieldValue<T, 'field_key'>` | a default response (e.g. `client.items.find(id)`) |
| `FieldValueInNestedResponse<T, 'field_key'>` | a nested response (e.g. `client.items.find(id, { nested: true })`) |

Each one accepts the same first argument in two equivalent forms:

-   **A value already in scope** — pass `typeof record` or `typeof block`. This is the common case when you've just fetched a record, or narrowed a nested block with `isBlockOfType`. No need to restate the model name.
-   **A model marker** — pass `Schema.X` directly. Use this when no value is in scope yet — typing a helper that builds a payload from scratch, or a function parameter that hasn't read anything.

**From a fetched value**

The same expression works on a top-level record and on a narrowed nested block:

```typescript
import {
  buildBlockRecord,
  type FieldValueInRequest,
  isBlockOfType,
} from '@datocms/cma-client';
import * as Schema from './schema';

const page = await client.items.find<Schema.LandingPage>(id, { nested: true });

const sections: NonNullable<FieldValueInRequest<typeof page, 'sections'>> = [];

for (const block of page.sections) {
  if (isBlockOfType(Schema.HeroBlock.ID, block)) {
    // Same expression, applied to a narrowed nested block.
    const ctas: NonNullable<FieldValueInRequest<typeof block, 'ctas'>> = [];
    // …rebuild ctas, then push the new HeroBlock into sections…
  } else {
    sections.push(block.id);
  }
}

await client.items.update<Schema.LandingPage>(page.id, { sections });
```

**From a model marker**

```typescript
import { buildBlockRecord, type FieldValueInRequest } from '@datocms/cma-client';
import * as Schema from './schema';

type Sections = NonNullable<FieldValueInRequest<Schema.Article, 'sections'>>;

function buildLaunchSections(headline: string): Sections {
  return [
    buildBlockRecord<Schema.ArticleSection>({
      item_type: Schema.ArticleSection.REF,
      title: headline,
    }),
  ];
}
```

> [!PROTIP] Typing whole payloads
> The `FieldValue*` helpers each materialize a single field's type. To annotate an **entire** create/update payload — or an entire response — reach for the corresponding `ApiTypes.*` shape (`ApiTypes.ItemCreateSchema<Schema.X>`, `ApiTypes.ItemUpdateSchema<Schema.X>`, `ApiTypes.Item<Schema.X>`, `ApiTypes.ItemInNestedResponse<Schema.X>`).

###### Narrowing a record or block to a specific model

When you have a value whose static type is a union of models (e.g. a record pulled from a mixed `list`, or a block from a Modular Content field), TypeScript needs to know *which* one you're holding before it'll let you reach into its attributes. The model ID is the natural way to tell them apart, but its canonical location (`relationships.item_type.data.id`) is nested four levels deep and TypeScript won't auto-narrow through that path.

Use the `isBlockOfType` helper from `@datocms/cma-client` instead — it's a proper type-guard predicate that works in both inline checks and array methods. It supports two equivalent calling styles:

```typescript
import { isBlockOfType } from "@datocms/cma-client";
import * as Schema from './schema';

const article = await client.items.find<Schema.Article>(articleId, {
  nested: true,
});

// 1. Inline check — pass both the model ID and the value.
for (const block of article.content) {
  if (isBlockOfType(Schema.HeroBlock.ID, block)) {
    block.attributes.headline; // OK — narrowed to HeroBlock
  }
}

// 2. As a predicate — pass just the model ID, get back a curried type guard
//    suitable for `.filter` / `.find`. A bare equality check here would NOT
//    narrow the result type.
const images = article.content.filter(isBlockOfType(Schema.ImageBlock.ID));
images[0].attributes.upload_id; // OK — narrowed to ImageBlock
```

> [!PROTIP] Skipping the helper for inline checks
> Every record and block in responses also carries a top-level `__itemTypeId` property, so `if (item.__itemTypeId === Schema.HeroBlock.ID)` narrows just as well as `isBlockOfType` for inline checks.

## Object payload

**`id`**

- Type: string
- Example: `"hWl-mnkWRYmMCSTq4z_piQ"`

RFC 4122 UUID of record expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"item"`.

**`meta.created_at`**

- Type: date-time

Date of creation

**`meta.updated_at`**

- Type: date-time

Last update time

**`meta.published_at`**

- Type: null, date-time

Date of last publication

**`meta.first_published_at`**

- Type: null, date-time

Date of first publication

**`meta.publication_scheduled_at`**

- Type: null, date-time

Date of future publication

**`meta.unpublishing_scheduled_at`**

- Type: null, date-time

Date of future unpublishing

**`meta.status`**

- Type: null, enum
- Example: `"published"`

Status

<details>
<summary>Show enum values</summary>

**`draft`**

The record is not published

**`updated`**

The record has some unpublished changes

**`published`**

The record is published

</details>

**`meta.is_current_version_valid`**

- Type: null, boolean

Whether the current version of the record is valid or not

**`meta.is_published_version_valid`**

- Type: null, boolean

Whether the published version of record is valid or not

**`meta.current_version`**

- Type: string
- Example: `"4234"`

The ID of the current record version

**`meta.stage`**

- Type: null, string

Workflow stage in which the item is

**`meta.has_children`**

- Type: null, boolean

When the records can be organized in a tree, indicates whether the record has children

**`item_type`**

- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

The record's model

**`creator`**

- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"access_token"\>](https://www-draft.datocms.com/docs/content-management-api/resources/access_token.md), [ResourceLinkage\<"user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/user.md), [ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

The entity (account/collaborator/access token/sso user) who created the record

<details>
<summary>Show deprecated</summary>

**`meta.is_valid`**

- Deprecated
- Type: boolean

Whether the current record is valid or not

This field will be removed in the future: use `is_current_version_valid` or `is_published_version_valid` instead, according to the specific use case

</details>

---

# Content Management API — List all records

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/instances.md

To retrieve a collection of records, send a GET request to the `/items` endpoint. The collection is [paginated](/docs/content-management-api/pagination.md), so make sure to iterate over all the pages if you need every record in the collection!

> [!PROTIP] 📚 New to DatoCMS records?
> Begin by reading the [Introduction to records](/docs/content-management-api/resources/item.md) guide to familiarize yourself with field types, API response modes, and the concepts of block manipulation!

## Filter combinations

You can use multiple filters to refine the records you want to retrieve. However, some combinations may be invalid, resulting in errors or ignored filters, which can lead to unexpected results:

-   `filter[ids]` cannot be combined with `filter[type]`, or `filter[fields]` related to model-specific fields
-   `filter[type]`, when specifying multiple item types, cannot be combined with `filter[ids]`, or `filter[fields]` related to model-specific fields
-   `filter[type]`, when specifying one (or more) block models, cannot be combined with `filter[ids]`, `filter[fields]`, or `query`

## Response modes: Regular vs. Nested

The `GET /items` endpoint, just like the [single record endpoint](/docs/content-management-api/resources/item/self.md), supports two different response modes that control how block fields are returned in the JSON payload. You can switch between them using the nested query parameter.

-   **Regular mode (default)**: This is the most efficient mode for listing multiple records. Any block fields (like Modular Content) will contain an array of **block IDs**, not the full block content. This keeps the response size small and fast.
-   **Nested mode (`nested=true`)**: This mode returns the complete content for any block fields. Instead of just IDs, the API will return full **block objects**, including all their attributes. This is useful when you need to display the blocks' content immediately without making additional API calls, or to read existing content and then make an update.

###### Example Regular mode (default)

Please note that if you don't specify any parameters, the API will return return the first 30 records. They can be from **any** model in your project.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string
 * └─ content: text
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const results = await client.items.list<Schema.Article>({
    version: "current",
  });

  console.log("-- LISTING ITEMS --");
  results.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

Showing the default number of results (30 records) from any model

```javascript
-- LISTING ITEMS --
└ Item "cEzRZmM3SyeHmAxS5KE9dg" (item_type: "T6TO3fbwRdyhcljYV8fyhg")
  ├ title: "Third Article"
  └ content: "This is the content of the third article."

└ Item "f2Ev8ybUTq6DFk348JeW1w" (item_type: "T6TO3fbwRdyhcljYV8fyhg")
  ├ title: "Second Article"
  └ content: "This is the content of the second article."

└ Item "VJ2-B9zJQPG-xdwU-vJdOw" (item_type: "T6TO3fbwRdyhcljYV8fyhg")
  ├ title: "First Article"
  └ content: "This is the content of the first article."
```


###### Example Nested mode

> [!WARNING] Lower limits apply with Nested Mode
> When `nested: true`, the maximum number of records you can request at once is restricted to 30, in contrast to the standard 500.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * └─ content: structured_text
 *    ├─ HeroBlock: headline, subtitle
 *    ├─ TextBlock: content
 *    └─ ImageBlock: image, caption
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const records = await client.items.list<Schema.BlogPost>({
    nested: true, // But retrieve its nested block content as well
    version: "current",
  });

  console.log("-- RECORDS WITH NESTED BLOCKS --");
  records.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

```javascript
-- RECORDS WITH NESTED BLOCKS --
└ Item "ZJ3ESh8pTBaRS5ED8loc2w" (item_type: "U4jwHd5-RCy2ItsDuWLMTQ")
  ├ title: "Understanding Modular Content"
  └ content
    ├ heading (level: 1)
    │ └ span "Welcome to Modular Content"
    ├ block
    │ └ Item "Sb5aNY5hQHaN_4KMU96H6A" (item_type: "GURToKAcQIyC-rTypPeHTw")
    │   ├ headline: "Hero Section"
    │   └ subtitle: "This is a hero block that introduces the content"
    ├ paragraph
    │ └ span "This blog post demonstrates how to work with structured text and modu..."
    ├ block
    │ └ Item "G_06E4v8SvqdChpFnEELxA" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
    │   └ content: "Modular content allows you to create flexible, reusable components that can b..."
    ├ block
    │ └ Item "T2lBvPruQwGAQTM9_ywm-g" (item_type: "ZsoQdbEVTkizaFPrama3IA")
    │   ├ image
    │   │ └ upload_id: "dLJdPzC3TW-mJXGo7K73Gg"
    │   └ caption: "A beautiful landscape showcasing the power of visual content"
    └ paragraph
      └ span "This concludes our example of nested blocks and structured content."

└ Item "T2lBvPruQwGAQTM9_ywm-g" (item_type: "ZsoQdbEVTkizaFPrama3IA")
  ├ image
  │ └ upload_id: "dLJdPzC3TW-mJXGo7K73Gg"
  └ caption: "A beautiful landscape showcasing the power of visual content"

└ Item "G_06E4v8SvqdChpFnEELxA" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  └ content: "Modular content allows you to create flexible, reusable components that can b..."

└ Item "Sb5aNY5hQHaN_4KMU96H6A" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ headline: "Hero Section"
  └ subtitle: "This is a hero block that introduces the content"
```


###### Example Filtering records by their creator (CMA only)

Filter records by their creator using the `_creator` meta field.

The `_creator` filter is **CMA-only** (not available via the [GraphQL Content Delivery API](/docs/content-delivery-api/filtering-records.md)). It accepts polymorphic creator references in the form `{ type, id }`, where `type` is one of:

-   `user` — a regular collaborator
-   `account` — the site owner (when owned by an account)
-   `organization` — the site owner (when owned by an organization)
-   `sso_user` — a SSO user
-   `access_token` — an API access token

Supported operators are `eq`, `neq`, `in`, and `notIn`. The `in` and `notIn` operators accept a list of references and may mix `type` values — for example, "everything created by these users *or* these access tokens".

> [!PROTIP] 🧭 Finding a creator's ID
> The shape mirrors what you find under `relationships.creator.data` on any record returned by the API — copy the `type` and `id` from there.

The `_creator` filter can also be used across multiple models in the same request (without `filter[type]`), making it convenient for "everything I created" cross-model queries.

In this example, we take the creator of an existing `blog_post` record and list all the records of that model created by the same creator.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Blog post
 * └─ title: string
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // A creator reference has the shape `{ type, id }`, the same you find under
  // `creator` on any record returned by the API. Here we take it from an
  // existing blog post, but you can also write it by hand, for example
  // `{ type: "user", id: "12345" }`.
  const [anyBlogPost] = await client.items.list<Schema.BlogPost>({
    filter: { type: "blog_post" },
    page: { limit: 1 },
  });

  if (!anyBlogPost?.creator) {
    throw new Error("No blog posts found");
  }

  const records = await client.items.list<Schema.BlogPost>({
    filter: {
      type: "blog_post",
      fields: {
        _creator: {
          eq: anyBlogPost.creator,
        },
      },
    },
    version: "current",
  });

  console.log(
    `-- BLOG POSTS CREATED BY ${anyBlogPost.creator.type} ${anyBlogPost.creator.id} --`,
  );
  records.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

```javascript
-- BLOG POSTS CREATED BY organization 628404 --
└ Item "KaMIqHpVSoKw9SN6P0zqjw" (item_type: "GURToKAcQIyC-rTypPeHTw")
  └ title: "Second post"

└ Item "ZJXenqgyTsungvb2mDLfNA" (item_type: "GURToKAcQIyC-rTypPeHTw")
  └ title: "Hello world"
```

## TypeScript typing

Iterating records without typed schemas means every attribute on every returned record is `unknown`, and filters on model-specific fields go unchecked. The single biggest lever you have is passing a generated `Schema.X` marker as the generic on `items.list` (or `items.listPagedIterator`). TypeScript then knows the exact shape of each returned record — its field names, types, and block structures — so reads are typed end-to-end:

```ts
import * as Schema from "./schema";

for await (const record of client.items.listPagedIterator<Schema.Article>({
  filter: { type: "article_model_id" },
})) {
  record.title; // typed, not unknown
}
```

For the exact type of a specific field on the returned records (to annotate a helper or intermediate variable), index `ApiTypes.Item<Schema.Article>["field_api_key"]` (or `ApiTypes.ItemInNestedResponse<Schema.Article>["field_api_key"]` when iterating with `nested: true`). See the [full TypeScript guide](https://www.datocms.com/cma-ts-schema.md) for how to generate `schema.ts` and the complete pattern.

The following table contains the list of all the possible arguments, along with their type, description and examples values.

## Query parameters

**`nested`**

- Type: boolean

For Modular Content, Structured Text and Single Block fields. If set, returns full payload for nested blocks instead of IDs

**`filter`**

- Type: object

Attributes to filter records

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"c89tCUarTvGKxA37acCEWA,aCiWeOsUT3mxY0KIzUfAhw"`

Record (or block record) IDs to fetch, comma separated. If you use this filter, you _must not_ use `filter[type]`. You can combine it with meta fields (like `_published_at`, `_status`, `_creator`), but _must not_ use model-specific fields

**`type`**

- Type: string
- Example: `"cat,dog"`

Model/Block model ID or `api_key` to filter. If you use this filter, you _must not_ use `filter[ids]`. When passing a single element, you can use both meta fields and model-specific fields (note: model-specific fields only work with models, not block models). When passing multiple comma-separated values, you can use meta fields but _must not_ use model-specific fields

**`query`**

- Type: string
- Example: `"foo"`

Textual query to match. Can be combined with other filters. When used, only records (not blocks) are returned. If `locale` is defined, search within that locale. Otherwise environment's main locale will be used.

**`fields`**

- Type: object
- Example: `{ name: { eq: "Buddy" } }`

Filter by record fields. Meta fields (like `_published_at`, `_status`, `_creator`) can be used in most cases. Model-specific fields (like `title`, `name`) require `filter[type]` to specify a single model, and only work with models (not block models). Same syntax as [GraphQL API records filters](/docs/content-delivery-api/filtering-records): use square brackets to indicate nesting levels. E.g. `filter[fields][parent][eq]=<ID_VALUE>`. Use snake_case for field names. The `_creator` meta filter is CMA-only (not available via the GraphQL API) and accepts polymorphic creator references in the form `{ "type": "user" | "account" | "organization" | "sso_user" | "access_token", "id": "<ID>" }`. If `locale` is defined, search within that locale. Otherwise environment's main locale will be used.

**`only_valid`**

- Type: string
- Example: `"true"`

When set, only valid records are included in the results.

</details>

**`locale`**

- Type: string
- Example: `"it"`

When `filter[query]` or `field[fields]` is defined, filter by this locale. Default: environment's main locale

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer
- Example: `200`

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 30, maximum is 500)

</details>

**`order_by`**

- Type: string
- Example: `"name_DESC"`

Fields used to order results. You **must** specify also `filter[type]` with one element only to be able use this option. Format: `<field_name>_(ASC|DESC)`, where `<field_name>` can be either the API key of a model's field, or one of the following meta columns: `id`, `_updated_at`, `_created_at`, `_status`, `_published_at`, `_first_published_at`, `_publication_scheduled_at`, `_unpublishing_scheduled_at`, `_is_valid`, `position` (only for sortable models). You can pass multiple comma separated rules.

**`version`**

- Type: string
- Example: `"published"`

Whether you want the currently published versions (`published`) of your records, or the latest available (`current`, default)

## Returns

Returns an array of resource objects of type [item](/docs/content-management-api/resources/item.md)

## Other examples

###### Example Fetching a specific page of records

To fetch a specific page, you can use the `page` object in the query params together with its `offset` and `limit` parameters. They will still be from **any model** in your project.

Code

To get 2 records starting from position 4, we should use: `limit: 2` and `offset: 3` (because record counting starts from 0)

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * ├─ content: text
 * └─ author: string
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const twoRecords = await client.items.list<Schema.BlogPost>({
    page: {
      limit: 2,
      offset: 3,
    },
    version: "current",
  });

  console.log("-- PAGINATED ITEMS --");
  twoRecords.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

```javascript
-- PAGINATED ITEMS --
└ Item "G9k168YbTietmuwlA7-evg" (item_type: "P--WAiOJQraTyQb3pwnNog")
  ├ title: "GraphQL Queries in DatoCMS"
  ├ content: "How to write efficient GraphQL queries to fetch your content."
  └ author: "Bob GraphQL"

└ Item "Wgx-4iIaTTWsjgj7kjxV1w" (item_type: "P--WAiOJQraTyQb3pwnNog")
  ├ title: "Using the Management API"
  ├ content: "A comprehensive guide to using the DatoCMS Content Management API."
  └ author: "Alice Engineer"
```


###### Example Fetching all pages

Instead of fetching a single page at a time, sometimes you want to get all the pages together.

You can do this using the `client.items.listPagedIterator()` method with an [async iteration statement](https://github.com/tc39/proposal-async-iteration#the-async-iteration-statement-for-await-of), which will handle pagination for you. All the details on how to use `listPagedIterator()` are outlined [on this page](/docs/content-management-api/pagination.md#paged-iterators).

Note that this will return records across **all** your models, unless you specify a filter. Unfiltered, this is useful for fetching all the records in your project (e.g. for backup or export purposes). To filter by IDs or models, see the other examples below.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Product
 * ├─ name: string
 * └─ price: float
 *
 * Category
 * ├─ name: string
 * └─ description: text
 *
 * Review
 * ├─ rating: integer
 * └─ comment: text
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // We'll be building up an array of all records using an AsyncIterator, `client.items.listPagedIterator()`
  const allRecords = [];

  for await (const record of client.items.listPagedIterator<
    Schema.Product | Schema.Category | Schema.Review
  >({ version: "current" })) {
    allRecords.push(record);
  }

  console.log("-- ALL RECORDS ACROSS ALL PAGES --");
  allRecords.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

Showing all results of `client.items.listPagedIterator()`, from any model

```javascript
-- ALL RECORDS ACROSS ALL PAGES --
└ Item "ApidnkdFSgSa0fMnxut7ug" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  ├ name: "Audio"
  └ description: "Audio equipment and sound devices"

└ Item "PeKN3Mx1QMiQpxR6rIfV1Q" (item_type: "ZsoQdbEVTkizaFPrama3IA")
  ├ rating: 5
  └ comment: "Excellent product, highly recommended!"

└ Item "Fo_aNiUGQce74zJQLghgEA" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  ├ name: "Electronics"
  └ description: "Electronic devices and accessories"

└ Item "DfAtrqEiQC-BpxRkCgFxug" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Bluetooth Speaker"
  └ price: 49.99

└ Item "XfVkvL9YT2K0KrhaqEXtrg" (item_type: "ZsoQdbEVTkizaFPrama3IA")
  ├ rating: 4
  └ comment: "Good quality, worth the price."

└ Item "d2InAHvfQze_HvxdtX3Iyw" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Wireless Headphones"
  └ price: 99.99
```


###### Example Fetching records by their IDs

You can retrieve a list of records (or blocks) by their record IDs. They can be from the same or different models.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Dog
 * ├─ name: string
 * └─ breed: string
 *
 * Song
 * ├─ title: string
 * ├─ artist: string
 * └─ duration: integer
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const records = await client.items.list<Schema.Dog | Schema.Song>({
    filter: {
      // Specific record IDs: one dog record, and one song record
      // Note that it's a comma-separated string with no spaces
      ids: "ZsoQdbEVTkizaFPrama3IA,U4jwHd5-RCy2ItsDuWLMTQ",
    },
    version: "current",
  });

  console.log("-- ITEMS BY SPECIFIC IDS --");
  records.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

Showing two records from different models: dog and song. The returned record order is random, not the order of the record IDs you specified.

```javascript
-- ITEMS BY SPECIFIC IDS --
└ Item "U4jwHd5-RCy2ItsDuWLMTQ" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  ├ title: "Bohemian Rhapsody"
  ├ artist: "Queen"
  └ duration: 355

└ Item "ZsoQdbEVTkizaFPrama3IA" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Buddy"
  └ breed: "Golden Retriever"
```


###### Example Fetching records belonging to a model

You can filter the records by one or more model types. You can use either the model's `api_key` (that you define) or its unique ID (generated by DatoCMS). Multiple comma-separated values are accepted:

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Cat
 * ├─ name: string
 * ├─ breed: string
 * └─ age: integer
 *
 * Dog
 * ├─ name: string
 * ├─ breed: string
 * └─ age: integer
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const records = await client.items.list<Schema.Cat | Schema.Dog>({
    filter: {
      // Filtering by the model with api_key "cat" and the model with ID of "dog"
      type: "cat,Y5l8wbEDQLKj7qm22CTucQ",
    },
    version: "current",
  });

  console.log("-- FILTERED BY MODEL TYPE --");
  records.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

```javascript
-- FILTERED BY MODEL TYPE --
└ Item "bVlcRGmFRqiK1nKPbesN8w" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Mittens"
  ├ breed: "Siamese"
  └ age: 2

└ Item "CEtirbQ_SnmHpowQ2tZ-ow" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  ├ name: "Max"
  ├ breed: "Labrador"
  └ age: 4

└ Item "KyueUsNkQfWh71dfxstVWQ" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  ├ name: "Buddy"
  ├ breed: "Golden Retriever"
  └ age: 5

└ Item "bRfo6K_YRJa1RrPfpUQTOg" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Whiskers"
  ├ breed: "Persian"
  └ age: 3
```


###### Example Fetching draft or updated records and filtering by publication status

By default, the API only returns published records. Using the `version` parameter, you can choose to also include drafts and updates.

`version: 'current'` will return the *most recent* versions of the queried records. Sometimes this can be the same as the published version, but other times it could be an unpublished draft or update:

-   `draft` means the record has been created and saved, but not yet published (or was unpublished)
-   `published` means the record has been published, and there are no later changes (i.e., the published version *is* the most recent version)
-   `updated` means the record was previously published, but there are new changes that have been saved and not yet published (the current version is *ahead* of the published version)

To get *only* draft, updated, or published records, you can filter on this response's `record.meta.status` property on the client side, *after* the fetch:

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Post
 * ├─ title: string
 * ├─ content: text
 * └─ author: string
 */

const getPublishedAndDraftRecordsOfModel = async () => {
  const client = buildClient({
    apiToken: process.env.DATOCMS_API_TOKEN,
  });

  const allRecords = await client.items.list<Schema.Post>({
    filter: {
      type: "post", // Model name or internal ID
    },
    version: "current", // Fetch the latest version of the records, regardless of publication status
  });

  // Records that have been saved but not published (or were unpublished later)
  const newDraftsOnly = allRecords.filter(
    (record) => record.meta.status === "draft",
  );

  // Records that were published but have unsaved changes ahead of the published version
  const updatedRecordsOnly = allRecords.filter(
    (record) => record.meta.status === "updated",
  );

  // Records that were published and have no further changes
  const publishedRecordsOnly = allRecords.filter(
    (record) => record.meta.status === "published",
  );

  console.log(`There are ${allRecords.length} total records in this model.`);
  console.log(`${publishedRecordsOnly.length} are published.`);
  console.log(`${updatedRecordsOnly.length} have unpublished updates.`);
  console.log(`${newDraftsOnly.length} are unpublished drafts.`);

  console.log("\n-- PUBLISHED RECORDS --");
  publishedRecordsOnly.forEach((item) => {
    console.log(inspectItem(item));
  });

  console.log("\n-- DRAFT RECORDS --");
  newDraftsOnly.forEach((item) => {
    console.log(inspectItem(item));
  });

  console.log("\n-- UPDATED RECORDS --");
  updatedRecordsOnly.forEach((item) => {
    console.log(inspectItem(item));
  });
};

getPublishedAndDraftRecordsOfModel();
```

Returned output

```javascript
There are 4 total records in this model.
2 are published.
1 have unpublished updates.
1 are unpublished drafts.

-- PUBLISHED RECORDS --
└ Item "Z1iG3QGISZON0p6ctQR7bg" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ title: "Another Published Post"
  ├ content: "This is another published post to show multiple published items."
  └ author: "Alice Publisher"

└ Item "Ssc2tQvNS-i0yjrcmq824A" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ title: "Published Article"
  ├ content: "This is a published article that's live on the website."
  └ author: "John Author"

-- DRAFT RECORDS --
└ Item "cjwt438ZT2ChDWpolIm2Sg" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ title: "Draft Article"
  ├ content: "This is a draft article that hasn't been published yet."
  └ author: "Jane Writer"

-- UPDATED RECORDS --
└ Item "YWrNII0ETpWdjXaO76AW3Q" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ title: "Updated Article [EDITED]"
  ├ content: "This article was published but then updated with new content."
  └ author: "Bob Editor"
```


###### Example Filtering a model's records by field values and sorting the results

Within a specified model, you can further filter its records by their field values.

You **must** specify a single model using `filter[type]`. You *cannot* filter by field value across multiple models at once.

Valid filters are documented at [GraphQL API records filters](/docs/content-delivery-api/filtering-records.md), so please check there. However, you **cannot** use [deep filtering](/docs/content-delivery-api/deep-filtering.md) on Modular Content and Structured Text fields at the moment.

-   You *may* add an optional `locale` parameter if you are filtering by a localized field.
-   You *may* add an optional `order_by` parameter.

In this example, we are filtering the model `dog` by:

-   A single-line string field, `name in ['Buddy','Rex']` (matching `Buddy` OR `Rex`)
-   A single-line string field, `breed eq 'mixed'` (matching exactly `mixed`)
-   A date field (`_updated_at`) (and ordering the results by the same)

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Dog
 * ├─ name: string
 * ├─ breed: string
 * ├─ age: integer
 * ├─ weight: float
 * └─ is_trained: boolean
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const records = await client.items.list<Schema.Dog>({
    filter: {
      type: "dog",
      fields: {
        name: {
          in: ["Buddy", "Rex"],
        },
        breed: {
          eq: "mixed",
        },
        _updated_at: {
          gt: "2020-04-18T00:00:00",
        },
      },
    },
    order_by: "_updated_at_ASC",
    version: "current",
  });

  console.log("-- FILTERED BY FIELD VALUES --");
  records.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

```javascript
-- FILTERED BY FIELD VALUES --
└ Item "bxiqfMHRTxaB1sv4bedQTQ" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Buddy"
  ├ breed: "mixed"
  ├ age: 5
  ├ weight: 25.5
  └ is_trained: true

└ Item "d9f4ZoN_T9WqJRtC-jVu_A" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Rex"
  ├ breed: "mixed"
  ├ age: 3
  ├ weight: 30.2
  └ is_trained: false
```


###### Example Fetching records by a textual generic query

You can retrieve a list of records filtered by a textual query match. It will search in block records content too. Set the `nested` parameter to `true` to retrieve embedded block content as well.

> [!WARNING] Content indexing delay
> Please note that you need to wait at least 30 seconds after creating or updating content before expecting to see results in textual queries.

You *can* narrow your search to some models by specifying the `filter[type]` parameter. You can use either the model's `api_key` or its unique ID. Multiple comma-separated values are accepted.

You *should* specify the `locale` attribute, or the environment's default locale will be used.

Returned records are ordered by rank.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Dog
 * ├─ name: string
 * ├─ description: text
 * └─ breed: string
 *
 * Cat
 * ├─ name: string
 * ├─ description: text
 * └─ breed: string
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const records = await client.items.list<Schema.Dog | Schema.Cat>({
    filter: {
      // optional, if defined, search in the specified models only
      type: "dog,cat",
      query: "chicken",
    },
    locale: "en",
    order_by: "_rank_DESC", // possible values: `_rank_DESC` (default) | `_rank_ASC`
    version: "current",
  });

  console.log("-- SEARCH RESULTS FOR 'chicken' --");
  records.forEach((item) => {
    console.log(inspectItem(item));
  });
}

run();
```

Returned output

```javascript
-- SEARCH RESULTS FOR 'chicken' --
└ Item "c2c5cXrITwOhj972XxEIsQ" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Luna"
  ├ description: "A playful Labrador puppy who enjoys swimming and chicken-flavored treats."
  └ breed: "Labrador"

└ Item "LlduEUcCSd-M2F5fZARUAw" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  ├ name: "Mittens"
  ├ description: "A curious Siamese cat who loves chicken and exploring high places."
  └ breed: "Siamese"

└ Item "L45_GuAKQCGfcNtLE4gjKA" (item_type: "Y5l8wbEDQLKj7qm22CTucQ")
  ├ name: "Shadow"
  ├ description: "A mysterious black cat who prefers fish over chicken and sleeps during the day."
  └ breed: "Domestic Shorthair"

└ Item "E0Y-kR8iSC-EooTe7dHx6w" (item_type: "GURToKAcQIyC-rTypPeHTw")
  ├ name: "Buddy"
  ├ description: "A friendly golden retriever who loves to play fetch and enjoys chicken treats..."
  └ breed: "Golden Retriever"
```

---

# Content Management API — Create a new record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/create.md

> [!PROTIP] 📚 New to DatoCMS records?
> Before creating your first record, we strongly recommend reading the [Introduction to Records](/docs/content-management-api/resources/item.md) guide. It covers fundamental concepts about field types, block manipulation, and localization that are essential for building a valid creation payload.

The payload required to create a new record is determined by the specific [model](/docs/content-management-api/resources/item-type.md) it's based on and the [fields](/docs/content-management-api/resources/field.md) it contains.

###### Example Basic example

This example demonstrates the basic process of creating a new record using the DatoCMS Content Management API. The example shows how to specify the item type and provide values for the record's fields.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Book
 * ├─ title: string
 * ├─ genre: string
 * ├─ synopsis: text
 * └─ pages: integer
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const record = await client.items.create<Schema.Book>({
    item_type: Schema.Book.REF,
    title: "The JavaScript Guide",
    genre: "Programming",
    synopsis:
      "A comprehensive guide to modern JavaScript.\nPerfect for beginners and experts alike.",
    pages: 450,
  });

  console.log(inspectItem(record));
}

run();
```

Returned output

```javascript
└ Item "E3eHzSH6QSGbTDmlKlSusw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "The JavaScript Guide"
  ├ genre: "Programming"
  ├ synopsis: "A comprehensive guide to modern JavaScript.\nPerfect for beginners and experts..."
  └ pages: 450
```

When creating a record, you don't need to specify a value for every field. Any field you omit will be set to its configured default value, or `null` if no default is set.

While the [Introduction to Records guide](/docs/content-management-api/resources/item.md) offers a complete reference for every field type, there are several key rules that are especially important when **creating** a new record.

### TypeScript typing

Writing a create payload without typed schemas means writing blind: every field is `unknown`, typos compile fine, and mistakes only surface as `422`s from the API. The single biggest lever you have is passing a [generated `Schema.X` marker](https://www.datocms.com/cma-ts-schema.md) as the generic on `items.create`. TypeScript then enforces the model's field names, types, and allowed block shapes at compile time:

```ts
// ❌ Untyped: every field is `unknown`, typos compile.
await client.items.create({ /* … */ });

// ✅ Typed: field names, types, and block shapes enforced.
await client.items.create<Schema.Article>({ /* … */ });
```

When you need the actual TypeScript type of a single field — to annotate a helper, an intermediate variable, or a function parameter — reach for `FieldValueInRequest<T, 'field_key'>`. The first argument is the `Schema.X` marker for the model that owns the field:

> [!WARNING] ⚠️ A marker can't be indexed directly
> `Schema.Article` is a phantom *type marker*, not a record shape. Writing `Schema.Article["content"]` won't give you the field type.
> 
> ```ts
> // ❌ Markers aren't indexable
> type Content = Schema.Article["content"];
> 
> 
> // ✅ Use the helper instead
> type Content = NonNullable<FieldValueInRequest<Schema.Article, "content">>;
> ```

```ts
function buildSections(
  page: ApiTypes.ItemInNestedResponse,
): NonNullable<FieldValueInRequest<Schema.LandingPage, "sections">> {
  // …assemble and return the sections array…
}

await client.items.create<Schema.LandingPage>({
  item_type: Schema.LandingPage.REF,
  title: "Product Launch Landing Page",
  sections: buildSections(sourcePage),
});
```

The first argument also accepts an item-shaped value the CMA already produced — useful when you're cloning or migrating from an existing record:

```ts
const source = await client.items.find<Schema.Article>("source-id", { nested: true });

if (source.content) {
  const content: NonNullable<FieldValueInRequest<typeof source, "content">> =
    /* …transform source.content… */;
  await client.items.create<Schema.Article>({ item_type: Schema.Article.REF, content });
}
```

> [!WARNING] ⚠️ Field values are always nullable
> Even fields with a **required** validator are typed as `Nullable`, because the API may still accept/return `null` in some scenarios. Wrap the helper in `NonNullable<…>` when you need the non-null shape.

> [!PROTIP] 📖 Read-side counterparts
> `FieldValueInRequest` is the *write*\-side helper. For values coming back from the API, use `FieldValue<T, 'field_key'>` (regular responses) or `FieldValueInNestedResponse<T, 'field_key'>` (when you fetched with `nested: true`). All three follow the same `<marker-or-value, fieldKey>` shape — see the [Item resource overview](/docs/content-management-api/resources/item.md) for the full helper family.

### Field value formatting

Every field in your payload must be formatted according to its type. This can range from a simple string or number to a structured object. For a comprehensive breakdown of the expected format for every field type, please refer to the **[Field Types Overview](/docs/content-management-api/resources/item.md#field-types-overview)** in our main records guide.

###### Example Managing simple fields

This example demonstrates how to create records with various simple field types, including text, numbers, dates, booleans, and more complex types like geo-location and color fields.

Key considerations when working with different field types:

-   **Geo-location fields**: Provide latitude and longitude as an object with both properties
-   **Color fields**: Specify RGBA values as an object with red, green, blue, and alpha components
-   **JSON fields**: Must be provided as a JSON-serialized string, not a JavaScript object
-   **Date/time fields**: Use ISO 8601 format strings for precise timestamps

For complete details on field value formats, see the [**Field types overview**](/docs/content-management-api/resources/item.md#field-types-overview) section.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Product
 * ├─ name: string
 * ├─ category: string
 * ├─ description: text
 * ├─ price: integer
 * ├─ weight: float
 * ├─ release_date: date_time
 * ├─ in_stock: boolean
 * ├─ warehouse_location: lat_lon
 * ├─ brand_color: color
 * └─ specifications: json
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const record = await client.items.create<Schema.Product>({
    item_type: Schema.Product.REF,
    name: "Premium Wireless Headphones",
    category: "Electronics",
    description:
      "High-quality noise-cancelling wireless headphones.\nPerfect for music and calls.",
    price: 299,
    weight: 0.25,
    release_date: "2024-03-15T10:30:00",
    in_stock: true,
    warehouse_location: {
      latitude: 45.0703393,
      longitude: 7.686864,
    },
    brand_color: {
      alpha: 255,
      blue: 156,
      green: 208,
      red: 239,
    },
    specifications: JSON.stringify({
      bluetooth: "5.0",
      battery_life: "30h",
      warranty: "2y",
    }),
  });

  console.log(inspectItem(record));
}

run();
```

Returned output

```javascript
└ Item "NZwcu3wYSgWeAbqVCJVToQ" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name: "Premium Wireless Headphones"
  ├ category: "Electronics"
  ├ description: "High-quality noise-cancelling wireless headphones.\nPerfect for music and calls."
  ├ price: 299
  ├ weight: 0.25
  ├ release_date: 2024-03-15T10:30:00+00:00
  ├ in_stock: true
  ├ warehouse_location
  │ ├ latitude: 45.0703393
  │ └ longitude: 7.686864
  ├ brand_color: #EFD09C
  └ specifications: {"bluetooth":"5.0","battery_life":"30h","warranty":"2y"}
```

#### Block Fields

When creating a record, any new blocks (for Modular Content, Single Block, or Structured Text fields) **must be provided as full block objects**. This object must include the `item_type` in its `relationships` to specify which Block Model to use. For a deeper dive into manipulating blocks, see the guide on **[Creating and Updating Blocks](/docs/content-management-api/resources/item.md#creating-and-updating-blocks)**.

###### Example Modular content fields

This example shows how to create records with modular content fields, which allow you to compose rich, dynamic content by combining multiple blocks of different types. Each block can have its own set of fields and can be repeated as needed.

The example uses the `buildBlockRecord()` helper function to create blocks more easily. You specify the block model ID and provide values for all the block's fields, similar to creating a regular record:

Code

```javascript
import {
  buildBlockRecord,
  buildClient,
  inspectItem,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * LandingPage
 * ├─ title: string
 * └─ sections: rich_text
 *    ├─ HeroBlock: headline, subtitle, background_image
 *    └─ TestimonialBlock: quote, author_name
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Create asset by using a local file:
  const upload = await client.uploads.createFromLocalFile({
    localPath: "./2018-10-17-194326.jpg",
  });

  const record = await client.items.create<Schema.LandingPage>({
    item_type: Schema.LandingPage.REF,
    title: "Product Launch Landing Page",
    sections: [
      // hero block
      buildBlockRecord<Schema.HeroBlock>({
        item_type: Schema.HeroBlock.REF,
        headline: "Revolutionary New Product",
        subtitle:
          "Transform your workflow with our cutting-edge solution.\nBuilt for modern teams.",
        background_image: { upload_id: upload.id },
      }),
      // testimonial block
      buildBlockRecord<Schema.TestimonialBlock>({
        item_type: Schema.TestimonialBlock.REF,
        quote: "This product completely transformed our business operations.",
        author_name: "Sarah Johnson, CEO",
      }),
    ],
  });

  console.log("-- Regular mode --");
  console.log(inspectItem(record));

  console.log("-- Nested mode --");
  const nestedRecord = await client.items.find<Schema.LandingPage>(record, {
    nested: true,
  });
  console.log(inspectItem(nestedRecord));
}

run();
```

Returned output

```javascript
-- Regular mode --
└ Item "Qw3wP3n8Qf286PjYiYzgXw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "Product Launch Landing Page"
  └ sections
    ├ [0]: "XhYlJCL5Rv-2UN2kRXnPcA"
    └ [1]: "S3hGBEOXTyKfOs6ac4o37Q"

-- Nested mode --
└ Item "Qw3wP3n8Qf286PjYiYzgXw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "Product Launch Landing Page"
  └ sections
    ├ [0] Item "XhYlJCL5Rv-2UN2kRXnPcA" (item_type: "T4m4tPymSACFzsqbZS65WA")
    │ ├ headline: "Revolutionary New Product"
    │ ├ subtitle: "Transform your workflow with our cutting-edge solution.\nBuilt for modern teams."
    │ └ background_image
    │   └ upload_id: "VUkBZ214TnK1UeHbJuoAmw"
    └ [1] Item "S3hGBEOXTyKfOs6ac4o37Q" (item_type: "JItInCQJSIeCLX3oGPvN1w")
      ├ quote: "This product completely transformed our business operations."
      └ author_name: "Sarah Johnson, CEO"
```


###### Example Single block fields

This example shows how to create records with single block fields, which allow you to add exactly one block to a field. Unlike modular content fields that can contain multiple blocks, single block fields hold either a single block instance or `null`.

The example uses the `buildBlockRecord()` helper function to create the block. You specify the block model ID and provide values for all the block's fields, similar to creating a regular record:

Code

```javascript
import {
  buildBlockRecord,
  buildClient,
  inspectItem,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * ProductPage
 * ├─ title: string
 * ├─ price: float
 * └─ hero_section: single_block
 *    └─ HeroBlock
 *       ├─ headline: string
 *       ├─ description: text
 *       ├─ button: single_block
 *       │  └─ ButtonBlock
 *       │     ├─ text: string
 *       │     └─ url: string
 *       └─ background_image: file
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Create asset by using a local file:
  const upload = await client.uploads.createFromLocalFile({
    localPath: "./hero-background.jpg",
  });

  const record = await client.items.create<Schema.ProductPage>({
    item_type: Schema.ProductPage.REF,
    title: "Premium Wireless Headphones",
    price: 299.99,
    hero_section: buildBlockRecord<Schema.HeroBlock>({
      item_type: Schema.HeroBlock.REF,
      headline: "Experience Audio Excellence",
      description:
        "Immerse yourself in crystal-clear sound with our premium wireless headphones.\nFeatures noise cancellation and 30-hour battery life.",
      button: buildBlockRecord<Schema.ButtonBlock>({
        item_type: Schema.ButtonBlock.REF,
        text: "Learn more",
        url: "/details",
      }),
      background_image: { upload_id: upload.id },
    }),
  });

  console.log("-- Regular mode --");
  console.log(inspectItem(record));

  console.log("-- Nested mode --");
  const nestedRecord = await client.items.find<Schema.ProductPage>(record, {
    nested: true,
  });
  console.log(inspectItem(nestedRecord));
}

run();
```

Returned output

```javascript
-- Regular mode --
└ Item "M54UhYRfQsSz62F1lTMHig" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "Premium Wireless Headphones"
  ├ price: 299.99
  └ hero_section: "R9Gt9gafTyaRSa8IFwWJEw"

-- Nested mode --
└ Item "M54UhYRfQsSz62F1lTMHig" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "Premium Wireless Headphones"
  ├ price: 299.99
  └ hero_section
    └ Item "R9Gt9gafTyaRSa8IFwWJEw" (item_type: "T4m4tPymSACFzsqbZS65WA")
      ├ headline: "Experience Audio Excellence"
      ├ description: "Immerse yourself in crystal-clear sound with our premium wireless headphones...."
      ├ button
      │ └ Item "aSFe8RhtThyajM5W3DEUPQ" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
      │   ├ text: "Learn more"
      │   └ url: "/details"
      └ background_image
        └ upload_id: "JP0pXTl3S0SFP42PYK2Mug"
```


###### Example Structured text fields

This example demonstrates how to create records with structured text fields that combine rich formatted text with embedded blocks. Structured text is perfect for editorial content where you need to mix paragraphs, headings, and interactive elements seamlessly.

The `buildBlockRecord()` helper function simplifies creating embedded blocks, and the example shows how to create upload resources for media content. For more upload creation methods, see the [Create a new upload](/docs/content-management-api/resources/upload/create.md) endpoint documentation.

Code

```javascript
import {
  buildBlockRecord,
  buildClient,
  inspectItem,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string
 * └─ content: structured_text
 *    ├─ CtaBlock: title, description, button_text, button_url
 *    └─ ImageGalleryBlock: title, images
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Create upload resources from URLs (or return existing uploads if already present in the media area):
  const upload1 = await client.uploads.createFromUrl({
    url: "https://picsum.photos/800/600?random=1",
    skipCreationIfAlreadyExists: true,
  });

  const upload2 = await client.uploads.createFromUrl({
    url: "https://picsum.photos/800/600?random=2",
    skipCreationIfAlreadyExists: true,
  });

  const record = await client.items.create<Schema.Article>({
    item_type: Schema.Article.REF,
    title: "The Future of Web Development",
    content: {
      schema: "dast",
      document: {
        type: "root",
        children: [
          // article introduction
          {
            type: "paragraph",
            children: [
              {
                type: "span",
                marks: [],
                value:
                  "Web development is rapidly evolving, and new technologies are reshaping how we build digital experiences. In this article, we'll explore the latest trends and tools that are defining the future.",
              },
            ],
          },
          // heading
          {
            type: "heading",
            level: 2,
            children: [
              {
                type: "span",
                marks: [],
                value: "Key Technologies to Watch",
              },
            ],
          },
          // paragraph with formatting
          {
            type: "paragraph",
            children: [
              {
                type: "span",
                marks: [],
                value: "From ",
              },
              {
                type: "span",
                marks: ["strong"],
                value: "serverless architectures",
              },
              {
                type: "span",
                marks: [],
                value: " to ",
              },
              {
                type: "span",
                marks: ["emphasis"],
                value: "edge computing",
              },
              {
                type: "span",
                marks: [],
                value:
                  ", developers now have unprecedented tools for building scalable applications.",
              },
            ],
          },
          // image gallery block
          {
            type: "block",
            item: buildBlockRecord<Schema.ImageGalleryBlock>({
              item_type: Schema.ImageGalleryBlock.REF,
              title: "Modern Development Environments",
              images: [
                {
                  upload_id: upload1.id,
                  alt: "Modern IDE setup",
                  title: "Development Environment",
                },
                {
                  upload_id: upload2.id,
                  alt: "Code collaboration tools",
                  title: "Team Collaboration",
                },
              ],
            }),
          },
          // another paragraph
          {
            type: "paragraph",
            children: [
              {
                type: "span",
                marks: [],
                value:
                  "As we look ahead, the integration of AI-powered tools and improved developer experiences will continue to accelerate innovation in our field.",
              },
            ],
          },
          // call-to-action block
          {
            type: "block",
            item: buildBlockRecord<Schema.CtaBlock>({
              item_type: Schema.CtaBlock.REF,
              title: "Ready to Level Up Your Skills?",
              description:
                "Join our community of forward-thinking developers and stay ahead of the curve with cutting-edge tutorials, tools, and insights.",
              button_text: "Join the Community",
              button_url: "https://example.com/join",
            }),
          },
        ],
      },
    },
  });

  console.log("-- Regular mode --");
  console.log(inspectItem(record));

  console.log("-- Nested mode --");
  const nestedRecord = await client.items.find<Schema.Article>(record, {
    nested: true,
  });
  console.log(inspectItem(nestedRecord));
}

run();
```

Returned output

```javascript
-- Regular mode --
└ Item "F50TH6XjSiqyMfezCmra0w" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "The Future of Web Development"
  └ content
    ├ paragraph
    │ └ span "Web development is rapidly evolving, and new technologies are reshapi..."
    ├ heading (level: 2)
    │ └ span "Key Technologies to Watch"
    ├ paragraph
    │ ├ span "From "
    │ ├ span (marks: strong) "serverless architectures"
    │ ├ span " to "
    │ ├ span (marks: emphasis) "edge computing"
    │ └ span ", developers now have unprecedented tools for building scalable appli..."
    ├ block "WWVn5YmfRfmwWr9rANi_Gg"
    ├ paragraph
    │ └ span "As we look ahead, the integration of AI-powered tools and improved de..."
    └ block "aciFN9_oRvWNjy7XIcm-9A"

-- Nested mode --
└ Item "F50TH6XjSiqyMfezCmra0w" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "The Future of Web Development"
  └ content
    ├ paragraph
    │ └ span "Web development is rapidly evolving, and new technologies are reshapi..."
    ├ heading (level: 2)
    │ └ span "Key Technologies to Watch"
    ├ paragraph
    │ ├ span "From "
    │ ├ span (marks: strong) "serverless architectures"
    │ ├ span " to "
    │ ├ span (marks: emphasis) "edge computing"
    │ └ span ", developers now have unprecedented tools for building scalable appli..."
    ├ block
    │ └ Item "WWVn5YmfRfmwWr9rANi_Gg" (item_type: "cSxBMd9XRWGGvq6xQ0qYDg")
    │   ├ title: "Modern Development Environments"
    │   └ images
    │     ├ [0]
    │     │ ├ upload_id: "LkyN_T-oTq-o0RGt9EDUMQ"
    │     │ ├ alt: "Modern IDE setup"
    │     │ └ title: "Development Environment"
    │     └ [1]
    │       ├ upload_id: "YptkFcGyRXi3QOTUymmm2A"
    │       ├ alt: "Code collaboration tools"
    │       └ title: "Team Collaboration"
    ├ paragraph
    │ └ span "As we look ahead, the integration of AI-powered tools and improved de..."
    └ block
      └ Item "aciFN9_oRvWNjy7XIcm-9A" (item_type: "T4m4tPymSACFzsqbZS65WA")
        ├ title: "Ready to Level Up Your Skills?"
        ├ description: "Join our community of forward-thinking developers and stay ahead of the curve..."
        ├ button_text: "Join the Community"
        └ button_url: "https://example.com/join"
```

#### Asset & Link Fields

These reference fields require specific formats. For a comprehensive breakdown of the expected format for every field type, please refer to the **[Field Types Overview](/docs/content-management-api/resources/item.md#field-types-overview)** in our main records guide.

###### Example Linking assets to records

This example shows how to create records that include image or file assets. You can work with both single assets and asset galleries by referencing existing uploads or creating new ones.

The example demonstrates two approaches:

-   **Single asset field**: Reference an upload with optional metadata overrides
-   **Asset gallery field**: Create arrays of asset objects with custom properties like alt text, title, focal points, and custom data

You can create new uploads from URLs using `client.uploads.createFromUrl()` or reference existing uploads from your media library. For more upload creation methods, see the [Create a new upload](/docs/content-management-api/resources/upload/create.md) endpoint documentation.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Portfolio
 * ├─ title: string
 * ├─ featured_image: file
 * └─ gallery: gallery
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // create upload resource using URL (or return an existing upload if it's already present in the media area):
  const upload1 = await client.uploads.createFromUrl({
    url: "https://picsum.photos/800/600?random=3",
    skipCreationIfAlreadyExists: true,
  });

  // create upload resource using local file (or return an existing upload if it's already present in the media area):
  const upload2 = await client.uploads.createFromLocalFile({
    localPath: "./local.jpg",
    skipCreationIfAlreadyExists: true,
  });

  const record = await client.items.create<Schema.Portfolio>({
    item_type: Schema.Portfolio.REF,
    title: "Architecture Photography",
    featured_image: {
      // in this case we're just passing the upload ID, as the
      // upload resource's defaults for alt, title, etc. are fine:
      upload_id: upload1.id,
    },
    gallery: [
      // here we want to override the upload resource's defaults:
      {
        upload_id: upload2.id,
        alt: "Modern architecture",
        title: "Urban Design",
        focal_point: {
          x: 0.3,
          y: 0.2,
        },
        custom_data: {
          add_watermark: true,
        },
      },
    ],
  });

  console.log(inspectItem(record));
}

run();
```

Returned output

```javascript
└ Item "LXN7jVhLTKaui_1dD9gLXA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "Architecture Photography"
  ├ featured_image
  │ └ upload_id: "HWhE3MhgTxCfqRfIMGW7Jg"
  └ gallery
    └ [0]
      ├ upload_id: "KJQT9BgrRjKJPALM9fsXgA"
      ├ alt: "Modern architecture"
      ├ title: "Urban Design"
      ├ custom_data: {"add_watermark":true}
      └ focal_point: x=30% y=20%
```


###### Example Linking records to other records

This example shows how to create records that reference other existing records through link fields. The process involves first retrieving the records you want to link to, then referencing them by their IDs when creating the new record.

Link fields can be either single links (referencing one record) or multiple links (referencing an array of records). The example demonstrates both scenarios and shows how to query for existing records before creating the relationships.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Author
 * ├─ name: string
 * ├─ collaborators: links
 * └─ mentor: link
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Let's retrieve some other authors first
  const otherAuthors = await client.items.list<Schema.Author>({
    filter: { type: "author" },
    version: "current",
  });

  if (otherAuthors.length === 0) {
    throw new Error("This example expects at least one record!");
  }

  const record = await client.items.create<Schema.Author>({
    item_type: Schema.Author.REF,
    name: "Sarah Johnson",
    collaborators: otherAuthors.map((author) => author.id),
    mentor: otherAuthors[0]!.id,
  });

  console.log(inspectItem(record));
}

run();
```

Returned output

```javascript
└ Item "Eukon4pbSrGyuYPWbmDiPQ" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name: "Sarah Johnson"
  ├ collaborators
  │ ├ [0]: "EF-CHNzIRAeFbt2ACY-K2Q"
  │ ├ [1]: "MeYQG9_HTEi0ZwhQi7kffA"
  │ └ [2]: "fHgaJ1AaTZm-U6hFdEthgw"
  └ mentor: "EF-CHNzIRAeFbt2ACY-K2Q"
```

### Localization

If the record's model contains localized fields, your creation payload must adhere to specific rules:

-   All localized fields in a single payload must specify the same set of locales to ensure consistency.
-   If the model is configured to require all locales ([`all_locales_required`](/docs/content-management-api/resources/item-type.md#object-payload)), then the payload must include a key for every available locale for each localized field. The value of a field for a locale can be `null`, but the key itself is mandatory.

For a full explanation of how to structure localized data, refer to the [localization Guide](/docs/content-management-api/resources/item.md#localization).

###### Example Managing localized fields

The code shows two scenarios:

1.  Providing content for a subset of available locales (`en`, `it`) with consistent locale keys across all localized fields
2.  Including all available locales (`en`, `it`, `fr`) even when some translations are `null`

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Pet
 * ├─ name: string (localized)
 * ├─ description: string (localized)
 * └─ category: string
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Example 1: Basic localization - consistent locales across all localized fields
  const basicRecord = await client.items.create<Schema.Pet>({
    item_type: Schema.Pet.REF,
    name: {
      en: "Sir Fluffington McWhiskers",
      it: "Signor Pelosetto Baffetti",
    },
    description: {
      en: "A noble cat with an impeccable mustache.",
      it: "Un gatto nobile con baffi impeccabili.",
    },
    // Non-localized field - single value for all locales
    category: "indoor",
  });

  console.log("-- Basic record --");
  console.log(inspectItem(basicRecord));

  // Example 2: When all_locales_required is true - must include all locales
  // even if some values are null
  const allLocalesRecord = await client.items.create<Schema.Pet>({
    item_type: Schema.Pet.REF,
    name: {
      en: "Luna",
      it: "Luna",
      fr: null, // Translation not ready yet, but key is required
    },
    description: {
      en: "A mysterious black cat that appears at midnight.",
      it: "Un gatto nero misterioso che appare a mezzanotte.",
      fr: null, // Translation not ready yet, but key is required
    },
    category: "outdoor",
  });

  console.log("-- All locales record --");
  console.log(inspectItem(allLocalesRecord));
}

run();
```

Returned output

```javascript
-- Basic record --
└ Item "PHz_ILZNR7uJ-r4PQsHEqg" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name
  │ ├ en: "Sir Fluffington McWhiskers"
  │ └ it: "Signor Pelosetto Baffetti"
  ├ description
  │ ├ en: "A noble cat with an impeccable mustache."
  │ └ it: "Un gatto nobile con baffi impeccabili."
  └ category: "indoor"

-- All locales record --
└ Item "OBvZ6gKbTOSGY7NGB_9fqA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name
  │ ├ en: "Luna"
  │ ├ fr: ""
  │ └ it: "Luna"
  ├ description
  │ ├ en: "A mysterious black cat that appears at midnight."
  │ ├ fr: ""
  │ └ it: "Un gatto nero misterioso che appare a mezzanotte."
  └ category: "outdoor"
```

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"hWl-mnkWRYmMCSTq4z_piQ"`

RFC 4122 UUID of record expressed in URL-safe base64 format

**`meta.created_at`**

- Optional
- Type: string

Date of creation

**`meta.first_published_at`**

- Optional
- Type: null, string

Date of first publication

**`item_type`**

- Required
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

The record's model

**`creator`**

- Optional
- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"access_token"\>](https://www-draft.datocms.com/docs/content-management-api/resources/access_token.md), [ResourceLinkage\<"user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/user.md), [ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

The entity (account/collaborator/access token/sso user) who created the record

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

## Other examples

###### Example Tree-like structure

This example demonstrates how to create records in tree-structured collections, where records can form hierarchical relationships with parent-child connections. Tree structures are useful for navigation menus, category hierarchies, organizational charts, and any content that needs nested organization.

When creating records in tree-like collections, you can specify:

-   **`parent_id`**: Links the record to its parent in the hierarchy
-   **`position`**: Sets the ordering among sibling records

The example shows how to build a hierarchy by creating records that reference each other:

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Category
 * ├─ name: string
 * ├─ position: integer
 * └─ parent_id: string
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const parent = await client.items.create<Schema.Category>({
    item_type: Schema.Category.REF,
    name: "Parent",
  });

  console.log(inspectItem(parent));

  const child1 = await client.items.create<Schema.Category>({
    item_type: Schema.Category.REF,
    name: "Child 1",
    parent_id: parent.id,
    position: 1,
  });

  console.log(inspectItem(child1));

  const child2 = await client.items.create<Schema.Category>({
    item_type: Schema.Category.REF,
    name: "Child 2",
    parent_id: parent.id,
    position: 2,
  });

  console.log(inspectItem(child2));
}

run();
```

Returned output

```javascript
└ Item "Ov5N1h4_QwW89_IzbUBb4g" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name: "Parent"
  ├ position: 1
  └ parent_id: null

└ Item "a8A-nl0cRXCqBNniQMAxog" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name: "Child 1"
  ├ position: 1
  └ parent_id: "Ov5N1h4_QwW89_IzbUBb4g"

└ Item "d6Gl9tPMSTqSF2bOMIcOYw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name: "Child 2"
  ├ position: 2
  └ parent_id: "Ov5N1h4_QwW89_IzbUBb4g"
```

---

# Content Management API — Duplicate a record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/duplicate.md

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "hWl-mnkWRYmMCSTq4z_piQ";

  const item = await client.items.duplicate(itemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(item);
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Update a record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/update.md

> [!PROTIP] 📚 New to DatoCMS records?
> We strongly recommend reading the [Introduction to Records](/docs/content-management-api/resources/item.md) guide first. The payload for updating a record follows the same structure as [creating one](/docs/content-management-api/resources/item/create.md), so that guide is also an essential prerequisite.

The fundamental rules for structuring field values (i.e., strings, numbers, objects, references) are the same for both creating and updating records. For a complete reference on how to format the value for every field type, please see the **[Field Types Overview](/docs/content-management-api/resources/item.md#field-types-overview)** in the main records guide.

**When updating an existing record, you only need to provide the fields you want to change. Any fields you omit from your payload will remain untouched.**

> [!WARNING] ⚠️ Null vs. Omitted Fields
> There's a crucial difference between omitting a field and explicitly setting it to `null` or an empty value:
> 
> -   **Omitted fields** keep their existing values unchanged
> -   **Fields set to `null` or empty values** (like `[]` for arrays) are cleared/deleted

###### Example Simple update operation

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * ├─ description: string
 * ├─ featured_image: file
 * └─ content_blocks: modular_content
 *    └─ HeroBlock: headline
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const record = await client.items.find<Schema.BlogPost>(
    "T4m4tPymSACFzsqbZS65WA",
    {
      nested: true,
    },
  );

  console.log("-- BEFORE UPDATE --");
  console.log(inspectItem(record));

  const item = await client.items.update<Schema.BlogPost>(
    "T4m4tPymSACFzsqbZS65WA",
    {
      title: "[EDIT] My first blog post!",
      featured_image: null,
      content_blocks: [],
    },
  );

  console.log("-- AFTER UPDATE --");
  console.log(inspectItem(item));
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "My first blog post!"
  ├ description: "An introduction to our new blog platform"
  ├ featured_image: null
  └ content_blocks
    └ [0] Item "WtzyjA4sTLiLiOQ9TBNgtQ" (item_type: "DB5xsyzCQ3iHTx0dZPb3sw")
      └ headline: "Hello!"

-- AFTER UPDATE --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "[EDIT] My first blog post!"
  ├ description: "An introduction to our new blog platform"
  ├ featured_image: null
  └ content_blocks: []
```

The following sections highlight the rules and strategies that are specific to the update process.

### TypeScript typing

Writing an update payload without typed schemas means writing blind: every field is `unknown`, typos compile fine, and mistakes only surface as `422`s from the API. The single biggest lever you have is passing a [generated `Schema.X` marker](https://www.datocms.com/cma-ts-schema.md) as the generic on `items.update`. TypeScript then enforces the model's field names, types, and allowed block shapes at compile time:

```ts
// ❌ Untyped: every field is `unknown`, typos compile.
await client.items.update("record-id", { /* … */ });

// ✅ Typed: field names, types, and block shapes enforced.
await client.items.update<Schema.Article>("record-id", { /* … */ });
```

When you're rebuilding a field value piece-by-piece — typically an array of blocks or links — annotate the accumulator with `FieldValueInRequest<T, 'field_key'>`. The first argument accepts any item-shaped value the CMA produces (top-level record or nested block, narrowed or not) **or** a `Schema.X` marker directly when no value is in scope yet:

```ts
const article = await client.items.find<Schema.Article>("record-id", { nested: true });

if (article.content) {
  const content: NonNullable<FieldValueInRequest<typeof article, "content">> =
    /* ...transform article.content... */;
  await client.items.update<Schema.Article>("record-id", { content });
}

// Same expression on a narrowed nested block:
for (const block of article.sections) {
  if (isBlockOfType(Schema.HeroBlock.ID, block)) {
    const ctas: NonNullable<FieldValueInRequest<typeof block, "ctas">> = [];
    // …rebuild the nested field…
  }
}

// Or, if you don't have a value yet, pass the marker:
type ArticleContent = NonNullable<FieldValueInRequest<Schema.Article, "content">>;
```

> [!WARNING] ⚠️ Field values are always nullable
> Even fields with a **required** validator are typed as `Nullable`, because the API may still accept/return `null` in some scenarios.
> 
> The example therefore checks `if (article.content)` and uses `NonNullable<…>` to narrow the type. Without this, accessing nested properties like `content.document.children` would require optional chaining (`?.`).

### Updating Block Fields

The general workflow is the same for every block field type:

1.  **Generate types** with the [TypeScript schema generator](https://www.datocms.com/cma-ts-schema.md).
2.  **Fetch records with `nested: true`** so blocks come back as full objects you can edit. Without it you only get IDs — fine for keep/reorder/delete, not for editing.
3.  **Build the payload** following the per-field-type rules below. Lean on the provided helpers as much as possible instead of assembling structures by hand.
4.  **Send a single `client.items.update<Article>` call**, omitting fields you don't change.

#### Modular Content

Payload is an array of blocks. Each entry expresses one operation:

| Operation | Entry |
| --- | --- |
| Keep | block ID string |
| Edit | `buildBlockRecord<T>({ id, ...changedAttrs })` |
| Create | `buildBlockRecord<T>({ ...allAttrs })` |
| Clone | `duplicateBlockRecord<T>(block, schemaRepository)` |
| Delete | omit from the array |
| Reorder | rearrange the array |

###### Example Managing blocks in Modular Content fields

Demonstrates every Modular Content operation in a single `items.update` call — keep, edit, create, clone, delete, and reorder all happen at once — and shows that the same accumulator pattern composes recursively when a block holds its own modular field:

-   **Create** — top of the array: `buildBlockRecord<CallToActionBlock>({ item_type, ...attrs })` (no `id`, since it's brand new).
-   **Clone** — duplicate the first existing testimonial with `duplicateBlockRecord<TestimonialBlock>(block, schemaRepository)`. The helper deep-copies and strips block IDs, so the result becomes a separate block rather than the same one moved.
-   **Delete** — `.filter(...)` drops the trailing "Start Your Free Trial" CTA. Any block missing from the new array is removed; missing IDs are how the API encodes deletion.
-   **Edit (flat)** — `buildBlockRecord<CallToActionBlock>({ id, button_url })` with only the changed attributes; omitted attributes (`button_text`) stay as-is on the server.
-   **Edit (nested)** — when the iterated block is a `HeroBlock`, rebuild *its* `ctas` modular array with the **same** accumulator pattern: `NonNullable<FieldValueInRequest<typeof block, 'ctas'>>` types the inner array, and the inner loop reuses `buildBlockRecord` / bare-ID-string the same way the outer loop does. The pattern composes uniformly to whatever depth the schema reaches.
-   **Keep** — return the bare ID string. Cheapest possible payload entry, used both at the top level and inside the nested `ctas`.
-   **Reorder** — implicit; the new array's order *is* the final order.

`isBlockOfType(ID)` is the curried predicate for `Array#filter` / `Array#find` and the inline guard inside `.map` callbacks — narrows `block.attributes` and `block.__itemTypeId` to the matching block model.

Code

```javascript
import {
  buildBlockRecord,
  buildClient,
  duplicateBlockRecord,
  type FieldValueInRequest,
  inspectItem,
  isBlockOfType,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * LandingPage
 * ├─ title: string
 * └─ sections: modular_content
 *    ├─ HeroBlock: headline, subtitle
 *    │  └─ ctas: modular_content
 *    │     └─ ButtonBlock: label, url
 *    ├─ CallToActionBlock: button_text, button_url
 *    └─ TestimonialBlock: quote, author
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaRepository = new SchemaRepository(client);

  const currentPage = await client.items.find<Schema.LandingPage>(
    "W4wcrs_2REiM4fc6dlDZCQ",
    { nested: true },
  );

  console.log("-- BEFORE UPDATE --");
  console.log(inspectItem(currentPage));

  const firstTestimonial = currentPage.sections.find(
    isBlockOfType(Schema.TestimonialBlock.ID),
  );
  const duplicatedTestimonial = firstTestimonial
    ? await duplicateBlockRecord<Schema.TestimonialBlock>(
        firstTestimonial,
        schemaRepository,
      )
    : null;

  const sections: NonNullable<
    FieldValueInRequest<typeof currentPage, "sections">
  > = [
    // Create — fresh CallToActionBlock at the top
    buildBlockRecord<Schema.CallToActionBlock>({
      item_type: Schema.CallToActionBlock.REF,
      button_text: "Watch a 2-minute demo",
      button_url: "https://www.datocms.com/demo",
    }),

    // Clone — duplicated testimonial
    ...(duplicatedTestimonial ? [duplicatedTestimonial] : []),

    ...currentPage.sections
      // Delete — drop the trailing CTA by filtering it out
      .filter(
        (block) =>
          !(
            isBlockOfType(Schema.CallToActionBlock.ID, block) &&
            block.attributes.button_text === "Start Your Free Trial"
          ),
      )
      .map((block) => {
        if (isBlockOfType(Schema.HeroBlock.ID, block)) {
          // Edit (nested) — rebuild the HeroBlock's `ctas` using the SAME
          // accumulator pattern, applied recursively to a nested block's
          // modular field.
          const ctas: NonNullable<FieldValueInRequest<typeof block, "ctas">> =
            block.attributes.ctas.map((cta) => {
              if (
                isBlockOfType(Schema.ButtonBlock.ID, cta) &&
                cta.attributes.label === "Get started"
              ) {
                const url = new URL(
                  cta.attributes.url || "https://www.datocms.com",
                );
                url.searchParams.set("utm_source", "hero");
                return buildBlockRecord<Schema.ButtonBlock>({
                  id: cta.id,
                  url: url.toString(),
                });
              }
              return cta.id;
            });

          return buildBlockRecord<Schema.HeroBlock>({ id: block.id, ctas });
        }

        if (isBlockOfType(Schema.CallToActionBlock.ID, block)) {
          // Edit (flat)
          const url = new URL(
            block.attributes.button_url || "https://www.datocms.com",
          );
          url.searchParams.set("utm_source", "landing_page");
          url.searchParams.set("utm_medium", "cta");
          url.searchParams.set("utm_campaign", "q1_2024");

          return buildBlockRecord<Schema.CallToActionBlock>({
            id: block.id,
            button_url: url.toString(),
          });
        }

        // Keep — return the bare ID for unchanged blocks
        return block.id;
      }),
  ];

  console.log("-- UPDATE OPERATION --");
  console.log(inspectItem({ sections }));

  await client.items.update<Schema.LandingPage>(currentPage, { sections });

  const updatedPage = await client.items.find<Schema.LandingPage>(currentPage, {
    nested: true,
  });

  console.log("-- AFTER UPDATE --");
  console.log(inspectItem(updatedPage));
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Product Launch Landing Page"
  └ sections
    ├ [0] Item "Ngzm6x8JS2y9UuQnTf9wBw" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
    │ ├ headline: "Revolutionary New Solution"
    │ ├ subtitle: "Discover the future of productivity with our cutting-edge platform designed f..."
    │ └ ctas
    │   ├ [0] Item "IfiDssuLSEa5HH2-Ce2nCw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │   │ ├ label: "Get started"
    │   │ └ url: "https://www.datocms.com/signup"
    │   └ [1] Item "IOggXsdxRAmFWCOPhbFXYw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │     ├ label: "Read the docs"
    │     └ url: "https://www.datocms.com/docs"
    ├ [1] Item "Iu1vi__ASDeDrrpPpKbknQ" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │ ├ button_text: "Get Started Free"
    │ └ button_url: "https://www.datocms.com/signup"
    ├ [2] Item "NxszxFtnSpmftjnxguWPOg" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
    │ ├ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
    │ └ author: "Sarah Chen, Product Manager at TechCorp"
    ├ [3] Item "MIJbkpwMQiqzwOYQkuAVIw" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
    │ ├ quote: "The best investment we've made this year. The ROI was evident within the firs..."
    │ └ author: "Michael Rodriguez, CTO at InnovateLabs"
    └ [4] Item "CVcgWRiaRYKF53DvqZHpFA" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
      ├ button_text: "Start Your Free Trial"
      └ button_url: "https://www.datocms.com/trial"

-- UPDATE OPERATION --
└ Item
  └ sections
    ├ [0] Item (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │ ├ button_text: "Watch a 2-minute demo"
    │ └ button_url: "https://www.datocms.com/demo"
    ├ [1] Item (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
    │ ├ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
    │ └ author: "Sarah Chen, Product Manager at TechCorp"
    ├ [2] Item "Ngzm6x8JS2y9UuQnTf9wBw"
    │ └ ctas
    │   ├ [0] Item "IfiDssuLSEa5HH2-Ce2nCw"
    │   │ └ url: "https://www.datocms.com/signup?utm_source=hero"
    │   └ [1] "IOggXsdxRAmFWCOPhbFXYw"
    ├ [3] Item "Iu1vi__ASDeDrrpPpKbknQ"
    │ └ button_url: "https://www.datocms.com/signup?utm_source=landing_page&utm_medium=cta&utm_cam..."
    ├ [4] "NxszxFtnSpmftjnxguWPOg"
    └ [5] "MIJbkpwMQiqzwOYQkuAVIw"

-- AFTER UPDATE --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Product Launch Landing Page"
  └ sections
    ├ [0] Item "O43QkMpUQFy4mTck48Ayeg" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │ ├ button_text: "Watch a 2-minute demo"
    │ └ button_url: "https://www.datocms.com/demo"
    ├ [1] Item "JQOSGafZTgK1cDWaFgtK9Q" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
    │ ├ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
    │ └ author: "Sarah Chen, Product Manager at TechCorp"
    ├ [2] Item "Ngzm6x8JS2y9UuQnTf9wBw" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
    │ ├ headline: "Revolutionary New Solution"
    │ ├ subtitle: "Discover the future of productivity with our cutting-edge platform designed f..."
    │ └ ctas
    │   ├ [0] Item "IfiDssuLSEa5HH2-Ce2nCw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │   │ ├ label: "Get started"
    │   │ └ url: "https://www.datocms.com/signup?utm_source=hero"
    │   └ [1] Item "IOggXsdxRAmFWCOPhbFXYw" (item_type: "TNTfjVnfQieREzX98q3xnw")
    │     ├ label: "Read the docs"
    │     └ url: "https://www.datocms.com/docs"
    ├ [3] Item "Iu1vi__ASDeDrrpPpKbknQ" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │ ├ button_text: "Get Started Free"
    │ └ button_url: "https://www.datocms.com/signup?utm_source=landing_page&utm_medium=cta&utm_cam..."
    ├ [4] Item "NxszxFtnSpmftjnxguWPOg" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
    │ ├ quote: "This platform completely transformed how our team collaborates. We've seen a ..."
    │ └ author: "Sarah Chen, Product Manager at TechCorp"
    └ [5] Item "MIJbkpwMQiqzwOYQkuAVIw" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
      ├ quote: "The best investment we've made this year. The ROI was evident within the firs..."
      └ author: "Michael Rodriguez, CTO at InnovateLabs"
```

#### Single Block

A single-block field holds at most one block (or `null`), so the payload is the value itself — not an array. The Modular Content rules apply, minus anything position-related.

###### Example Managing Single Block fields

Demonstrates every Single Block operation across multiple `items.update` calls — the field holds at most one block (or `null`):

-   **Edit** — `buildBlockRecord<CallToActionBlock>({ id, button_text, style })` keeps the same block ID, changes the listed attributes, leaves the rest (`button_url`) untouched.
-   **Replace via duplicate** — `duplicateBlockRecord<HeroBlock | CallToActionBlock | VideoBlock>(currentProduct.hero_section, schemaRepository)` deep-copies and strips IDs; the slot ends up with a brand-new block, not the same one re-used.
-   **Replace with a different block type** — `buildBlockRecord<VideoBlock>({ item_type, ...attrs })` swaps the slot's model entirely (no `id`, since the block doesn't exist yet).
-   **Delete** — set the field to `null`. Omitting the field would leave the existing block in place (see the parent guide's null-vs-omit warning).

The response uses `__itemTypeId` as a discriminant: narrowing on it (`if (currentProduct.hero_section.__itemTypeId === CTA_ID)`) lets TypeScript see the matching block-model attributes before any string ops.

Code

```javascript
import {
  type ApiTypes,
  buildBlockRecord,
  buildClient,
  duplicateBlockRecord,
  inspectItem,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * ProductPage
 * ├─ title: string
 * ├─ price: float
 * └─ hero_section: single_block
 *    ├─ HeroBlock: headline, description, background_image
 *    ├─ CallToActionBlock: button_text, button_url, style
 *    └─ VideoBlock: video_url, thumbnail_image, autoplay
 */

// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

async function run() {
  const schemaRepository = new SchemaRepository(client);

  const currentProduct = await client.items.find<Schema.ProductPage>(
    "W4wcrs_2REiM4fc6dlDZCQ",
    { nested: true },
  );

  console.log("-- BEFORE UPDATE --");
  console.log(inspectItem(currentProduct));

  if (
    currentProduct.hero_section &&
    currentProduct.hero_section.__itemTypeId === Schema.CallToActionBlock.ID
  ) {
    await client.items.update<Schema.ProductPage>(currentProduct, {
      hero_section: buildBlockRecord<Schema.CallToActionBlock>({
        id: currentProduct.hero_section.id,
        button_text:
          currentProduct.hero_section.attributes.button_text?.toUpperCase() ||
          "SHOP NOW",
        style: "primary-large",
      }),
    });
    console.log("-- EXISTING BLOCK UPDATED --");
    await inspectItemWithNestedBlocks(currentProduct);
  }

  await client.items.update<Schema.ProductPage>(currentProduct, {
    hero_section: await duplicateBlockRecord<
      Schema.HeroBlock | Schema.CallToActionBlock | Schema.VideoBlock
    >(currentProduct.hero_section!, schemaRepository),
  });
  console.log("-- BLOCK DUPLICATE --");
  await inspectItemWithNestedBlocks(currentProduct);

  const upload = await client.uploads.createFromUrl({
    url: "https://picsum.photos/800/600?random=1",
  });
  const productWithVideo = await client.items.update<Schema.ProductPage>(
    currentProduct,
    {
      hero_section: buildBlockRecord<Schema.VideoBlock>({
        item_type: Schema.VideoBlock.REF,
        video_url: "https://videos.datocms.com/product-demo.mp4",
        thumbnail_image: { upload_id: upload.id },
        autoplay: false,
      }),
    },
  );
  console.log("-- BLOCK REPLACED --");
  await inspectItemWithNestedBlocks(currentProduct);

  await client.items.update<Schema.ProductPage>(productWithVideo, {
    hero_section: null,
  });
  console.log("-- BLOCK REMOVED --");
  await inspectItemWithNestedBlocks(currentProduct);
}

run();

async function inspectItemWithNestedBlocks(item: ApiTypes.Item) {
  const itemWithNestedBlocks = await client.items.find(item, {
    nested: true,
  });
  console.log(inspectItem(itemWithNestedBlocks));
}
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Premium Wireless Headphones"
  ├ price: 299.99
  └ hero_section
    └ Item "MolF0AwpSLeXrdE5kdcEtw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
      ├ button_text: "Buy Now"
      ├ button_url: "https://example.com/buy"
      └ style: "primary"

-- EXISTING BLOCK UPDATED --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Premium Wireless Headphones"
  ├ price: 299.99
  └ hero_section
    └ Item "MolF0AwpSLeXrdE5kdcEtw" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
      ├ button_text: "BUY NOW"
      ├ button_url: "https://example.com/buy"
      └ style: "primary-large"

-- BLOCK DUPLICATE --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Premium Wireless Headphones"
  ├ price: 299.99
  └ hero_section
    └ Item "QXqFgPHVTfq8F1tOmQEwEg" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
      ├ button_text: "Buy Now"
      ├ button_url: "https://example.com/buy"
      └ style: "primary"

-- BLOCK REPLACED --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Premium Wireless Headphones"
  ├ price: 299.99
  └ hero_section
    └ Item "JIHRl3kyQiGXAJHcP-7v7Q" (item_type: "Dy9C52o4S6eF3mqSOmeUtg")
      ├ video_url: "https://videos.datocms.com/product-demo.mp4"
      ├ thumbnail_image
      │ └ upload_id: "WwqHexgISQqQdMKJSYE8VA"
      └ autoplay: false

-- BLOCK REMOVED --
└ Item "W4wcrs_2REiM4fc6dlDZCQ" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "Premium Wireless Headphones"
  ├ price: 299.99
  └ hero_section: null
```

#### Structured Text

Structured Text stores rich content as a [DAST](/docs/structured-text/dast.md) tree. Embedded records show up as two extra node kinds: `block` and `inlineBlock`. Both wrap a full DatoCMS block, so the same `buildBlockRecord`/`duplicateBlockRecord` primitives from Modular Content and Single Block still apply, but they're composed inside a tree-mutation pipeline rather than dropped into an array slot.

The pipeline has two passes, each exposing the document at a different level of granularity. Pick the highest-level pass that can still see what you want to change. Going lower buys you more power but costs you a lot of tree gymnastics.

###### Pass 1: Rewrite the prose (dastdown)

[`datocms-structured-text-dastdown`](https://github.com/datocms/structured-text/tree/main/packages/dastdown) translates the DAST tree to and from a markdown-like text format:

```markdown
A normal paragraph with ==highlighted==, ++underlined++ and ~~struck~~ text.

Links can carry trailers: [our docs](https:datocms.com){target="_blank"}.
References to other records read like [this article](dato:item/abc123),
or inline as <inlineItem id="abc123"/>.

> A pull quote from the interview.
> {attribution="Jane Doe"}

<block id="def456"/>

Inline blocks <inlineBlock id="ghi789"/> sit mid-sentence.
```

This allows you to make changes with ordinary string operations and let `parse()` rebuild the tree:

```ts
import { parse, serialize } from "datocms-structured-text-dastdown";

const text = serialize(currentRecord.content);

const edited = text
  .replace(/Jane Doe/g, "Jane Smith")
  .replace(/==([^=]+)==/g, "**$1**");

const content = parse(edited, currentRecord.content);
```

Blocks appear in dastdown only as ID placeholders: you can move or delete them, but you can't touch what's inside.

If you can describe the change as something you'd do in a text editor — find-and-replace, rewriting a paragraph, reshaping a list — `dastdown` is the right tool. It also wins on the opposite end of the spectrum from Pass 2: one-off spot edits, and agentic flows where an LLM reads the serialized text and rewrites it directly.

###### Pass 2: Mutate the tree (`mapNodes` + block helpers)

Reach for Pass 2 when dastdown can't see what you want to change — anything that depends on node *kind* rather than text, or on a block's *internal fields* rather than its position. Both flavors of edit live inside the same `mapNodes` walk:

-   **Transforming prose nodes.** Rewrite every link's URL, lowercase every heading, bump every `level: 2` heading to `level: 3`, drop every empty paragraph, wrap every occurrence of "click here" in a link. dastdown is the right tool while edits stay mostly text-shaped; once you find yourself writing regex to fish node kinds back out of the serialized form, the AST is the cleaner layer.
-   **Editing or building embedded blocks.** Edit a block's attributes, swap it for another, or drop a brand-new block into the tree. Embedded blocks are opaque to Pass 1 (dastdown serializes them to `<block id="…"/>` placeholders that hide their fields), so anything touching block contents lands here. Use `buildBlockRecord<T>` to shape the payload — pass an `id` to edit an existing block, omit it to create a new one — and `duplicateBlockRecord<T>` to deep-clone one.

`mapNodes` from `datocms-structured-text-utils` walks the tree **bottom-up** (a node's descendants have already been transformed by the time the callback sees it, and what you return for that node is final). Return one node (1:1, the default), an array splatted into siblings (1:N — split, wrap, insert), or `null`/`undefined` to drop (1:0 — illegal at the root).

When a node doesn't need to change, just `return node`. The CMA accepts the nested-response shape it came in as.

**Adding root-level nodes** (a brand-new paragraph, a fresh top-level block) sits just outside the callback: `mapNodes` can't splat at the root, so push directly into `content.document.children` after the walk — using `buildBlockRecord` / `duplicateBlockRecord` to shape any new `block` entry.

> [!WARNING] ⚠️ Combine passes in order: 1 → 2
> Pass 1's `parse()` uses the *original* document as the lookup table for `<block id="…"/>` placeholders. A block created by Pass 2 first would either be missing from that lookup (and `parse` throws) or get silently overwritten when Pass 1 rehydrates. If you need both, always run Pass 1 before Pass 2.

###### Example Pass 1: Prose edits via dastdown

Demonstrates Pass 1 (dastdown round-trip) on a structured-text field with no embedded blocks. Three text-level transformations expressed as plain string operations:

-   **Brand swap** — `text.replace(/ZEIT/g, "Vercel")` rewrites every occurrence across spans, headings, and link text in a single regex.
-   **Autolink emails** — turn bare addresses into markdown links: `support@example.com` → `[support@example.com](mailto:support@example.com)`.
-   **Set `target="_blank"` on a specific link** — append the dastdown link-meta trailer `{target="_blank"}` to the matching `(url)`. The negative lookahead `(?!\{)` makes the regex idempotent (a second run is a no-op).

Block placeholders aren't relevant here, but `parse(text, currentGuide.body)` always uses the second argument as the `<block id="…"/>` lookup table; missing IDs throw, which is the signal to fall back to Pass 2.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import { parse, serialize } from "datocms-structured-text-dastdown";
import type * as Schema from "./schema.js";

/*
 * Guide
 * ├─ title: string
 * └─ body: structured_text (no embedded blocks for this example)
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const currentGuide = await client.items.find<Schema.Guide>(
    "Q9zHRrIESkGYBV3hVVe2Hg",
  );

  // `currentGuide.body` is typed nullable — guard before touching the document.
  // Wrapping in `if (currentGuide.body) { ... }` also avoids a top-level
  // `return`, letting peek + mutate live in a single script.
  if (currentGuide.body) {
    console.log("-- BEFORE UPDATE --");
    console.log(inspectItem(currentGuide));

    const text = serialize(currentGuide.body);

    const edited = text
      .replace(/ZEIT/g, "Vercel")
      .replace(/(\b[\w.+-]+@[\w-]+\.[\w.-]+\b)/g, "[$1](mailto:$1)")
      .replace(
        /\(https:\/\/example\.com\/migration\)(?!\{)/g,
        '(https://example.com/migration){target="_blank"}',
      );

    // 2nd arg is the lookup table for `<block id="…"/>` placeholders; missing IDs throw.
    const body = parse(edited, currentGuide.body);

    await client.items.update<Schema.Guide>(currentGuide.id, { body });

    console.log("-- AFTER UPDATE --");
    console.log(
      inspectItem(await client.items.find<Schema.Guide>(currentGuide.id)),
    );
  }
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "Q9zHRrIESkGYBV3hVVe2Hg" (item_type: "f6sjkkPgQGiSDi6lwG3UjA")
  ├ title: "Deploying with ZEIT"
  └ body
    ├ heading (level: 1)
    │ └ span "Deploying with ZEIT"
    ├ paragraph
    │ └ span "ZEIT lets you ship static sites and serverless functions in seconds. ..."
    └ paragraph
      ├ span "Read our "
      ├ link (url: "https://example.com/migration")
      │ └ span "migration guide"
      └ span " before upgrading from ZEIT v1."

-- AFTER UPDATE --
└ Item "Q9zHRrIESkGYBV3hVVe2Hg" (item_type: "f6sjkkPgQGiSDi6lwG3UjA")
  ├ title: "Deploying with ZEIT"
  └ body
    ├ heading (level: 1)
    │ └ span "Deploying with Vercel"
    ├ paragraph
    │ ├ span "Vercel lets you ship static sites and serverless functions in seconds..."
    │ ├ link (url: "mailto:support@zeit.co")
    │ │ └ span "support@zeit.co"
    │ └ span " for help."
    └ paragraph
      ├ span "Read our "
      ├ link (url: "https://example.com/migration", meta: {target="_blank"})
      │ └ span "migration guide"
      └ span " before upgrading from Vercel v1."
```


###### Example Pass 2: Node transformations

Demonstrates Pass 2 on prose nodes — `mapNodes` with one of each return mode (1:1 transforms, 1:0 drop) plus a post-walk root-level append. The callback applies five edits across the tree:

-   **Demote `h1` → `h2`** for hierarchy hygiene (`as const` keeps the literal `level` type).
-   **Bold every span that mentions the brand**, deduping the `marks` array with a `Set` (canonical marks: `'strong' | 'emphasis' | 'code' | 'underline' | 'strikethrough' | 'highlight'`).
-   **Brand swap** — `value.replace(/ZEIT/g, "Vercel")` on every matching span.
-   **Add `target="_blank"`** to `link` and `itemLink` nodes, deduping any prior `target` entry in `meta`.
-   **Drop empty paragraphs** by returning `null` from the callback.

After `mapNodes` returns, a fresh paragraph is pushed into `content.document.children` — root-level inserts can't go through the callback (splat-at-root throws).

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import {
  isHeading,
  isItemLink,
  isLink,
  isParagraph,
  isSpan,
  mapNodes,
  reduceNodes,
} from "datocms-structured-text-utils";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * ├─ slug: string
 * └─ content: structured_text (no embedded blocks for this example)
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const currentPost = await client.items.find<Schema.BlogPost>(
    "T4m4tPymSACFzsqbZS65WA",
  );

  // Guard the nullable field — also keeps peek + mutate in a single script
  // without a top-level `return`.
  if (currentPost.content) {
    console.log("-- BEFORE UPDATE --");
    console.log(inspectItem(currentPost));

    const content = mapNodes(currentPost.content, (node) => {
      if (isHeading(node) && node.level === 1) {
        // `as const` preserves the literal `2` instead of widening to `number`.
        return { ...node, level: 2 as const };
      }

      if (isSpan(node) && node.value.includes("ZEIT")) {
        const marks = new Set(node.marks ?? []);
        marks.add("strong");
        return {
          ...node,
          value: node.value.replace(/ZEIT/g, "Vercel"),
          marks: [...marks],
        };
      }

      if (isLink(node) || isItemLink(node)) {
        const meta = [
          ...(node.meta ?? []).filter((m) => m.id !== "target"),
          { id: "target", value: "_blank" },
        ];
        return { ...node, meta };
      }

      if (
        isParagraph(node) &&
        reduceNodes(
          node,
          (acc, n) => (isSpan(n) ? acc + n.value.trim() : acc),
          "",
        ).length === 0
      ) {
        return null;
      }

      return node;
    });

    // Root-level inserts can't go through `mapNodes` (splat-at-root throws); push directly.
    content.document.children.push({
      type: "paragraph",
      children: [{ type: "span", value: "Last updated by the content team." }],
    });

    await client.items.update<Schema.BlogPost>(currentPost.id, { content });

    console.log("-- AFTER UPDATE --");
    console.log(
      inspectItem(await client.items.find<Schema.BlogPost>(currentPost.id)),
    );
  }
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "What is ZEIT?"
  ├ slug: "what-is-zeit"
  └ content
    ├ heading (level: 1)
    │ └ span "Understanding ZEIT"
    ├ paragraph
    │ └ span "ZEIT is a cloud platform for static sites and serverless functions. I..."
    ├ paragraph
    │ └ span ""
    ├ paragraph
    │ └ span ""
    ├ heading (level: 1)
    │ └ span "Key Features"
    ├ paragraph
    │ ├ span "ZEIT offers automatic HTTPS, global CDN distribution, and instant dep..."
    │ ├ link (url: "https://example.com/blog")
    │ │ └ span "detailed comparison"
    │ └ span " for more insights."
    └ paragraph
      ├ span "Visit our "
      ├ itemLink (item: "fpgJWZadRI66eqXB-ucSSQ")
      │ └ span "migration guide"
      └ span " for step-by-step instructions."

-- AFTER UPDATE --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "What is ZEIT?"
  ├ slug: "what-is-zeit"
  └ content
    ├ heading (level: 2)
    │ └ span (marks: strong) "Understanding Vercel"
    ├ paragraph
    │ └ span (marks: strong) "Vercel is a cloud platform for static sites and serverless functions...."
    ├ heading (level: 2)
    │ └ span "Key Features"
    ├ paragraph
    │ ├ span (marks: strong) "Vercel offers automatic HTTPS, global CDN distribution, and instant d..."
    │ ├ link (url: "https://example.com/blog", meta: {target="_blank"})
    │ │ └ span "detailed comparison"
    │ └ span " for more insights."
    ├ paragraph
    │ ├ span "Visit our "
    │ ├ itemLink (item: "fpgJWZadRI66eqXB-ucSSQ", meta: {target="_blank"})
    │ │ └ span "migration guide"
    │ └ span " for step-by-step instructions."
    └ paragraph
      └ span "Last updated by the content team."
```


###### Example Pass 2 (cont.): Editing & creating embedded blocks

Demonstrates Pass 2 on embedded blocks — editing block attributes from inside a `mapNodes` callback, then duplicating and appending a block after the walk. What the script does:

-   **Edit CTA blocks**: rewrite `button_url` from `old-domain.com` to `new-domain.com`, returning `{ ...node, item: buildBlockRecord<CtaBlock>({ id, button_url }) }` from the `mapNodes` callback.
-   **Tag inline product mentions**: append `?source=article_mention` to `affiliate_url` with the same pattern, narrowing via `isInlineBlockWithItemOfType`.
-   **Pass through unmatched blocks** (the `ImageGalleryBlock` here) with a bare `return node` — the CMA accepts the nested-response shape it came in as.
-   **Duplicate the first CTA** with `duplicateBlockRecord<CtaBlock>` and push it as a new root-level child, *after* `mapNodes` runs.

A few things to notice:

-   **`isBlockWithItemOfType` has two call styles.** `isBlockWithItemOfType(ID, node)` is the inline form for `if`; the curried form `isBlockWithItemOfType(ID)` is the predicate for `findFirstNode` / `Array#find` / `Array#filter`. Same guard, different ergonomics. With `ID` declared `as const`, both auto-narrow `node.item` to the matching block-model shape.
-   **Source the duplicate from the original tree.** Look up the source block on `currentArticle.content` (the original response), not on the mapped result — `mapNodes` is allowed to rewrite `node.item`, so the post-map tree is not a reliable source for cloning.

Code

```javascript
import {
  buildBlockRecord,
  buildClient,
  duplicateBlockRecord,
  type FieldValueInRequest,
  inspectItem,
  SchemaRepository,
} from "@datocms/cma-client-node";
import {
  findFirstNode,
  isBlockWithItemOfType,
  isInlineBlockWithItemOfType,
  mapNodes,
} from "datocms-structured-text-utils";
import * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string
 * ├─ author: string
 * └─ content: structured_text
 *    ├─ CtaBlock: title, description, button_text, button_url
 *    ├─ ProductMentionInline: product_name, price, affiliate_url
 *    └─ ImageGalleryBlock: title, images
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // `duplicateBlockRecord` looks up nested-block field definitions through this.
  const repo = new SchemaRepository(client);

  const currentArticle = await client.items.find<Schema.Article>(
    "RSfdsZbbR7ixGgMBSmcaVA",
    {
      nested: true,
    },
  );

  // Guard the nullable field — also keeps peek + mutate in a single script
  // without a top-level `return`.
  if (currentArticle.content) {
    console.log("-- BEFORE UPDATE --");
    console.log(inspectItem(currentArticle));

    let content: NonNullable<
      FieldValueInRequest<typeof currentArticle, "content">
    > = currentArticle.content;

    content = mapNodes(content, (node) => {
      if (isBlockWithItemOfType(Schema.CtaBlock.ID, node)) {
        const url = node.item.attributes.button_url;
        if (url?.includes("old-domain.com")) {
          return {
            ...node,
            item: buildBlockRecord<Schema.CtaBlock>({
              id: node.item.id,
              button_url: url.replace("old-domain.com", "new-domain.com"),
            }),
          };
        }
      }

      if (isInlineBlockWithItemOfType(Schema.ProductMentionInline.ID, node)) {
        const raw = node.item.attributes.affiliate_url;
        if (raw) {
          const url = new URL(raw);
          url.searchParams.set("source", "article_mention");
          return {
            ...node,
            item: buildBlockRecord<Schema.ProductMentionInline>({
              id: node.item.id,
              affiliate_url: url.toString(),
            }),
          };
        }
      }

      return node;
    });

    // Source the duplicate from the original tree: `mapNodes` may have
    // rewritten `node.item`, so post-map `content` is not safe to clone from.
    const firstCta = findFirstNode(
      currentArticle.content,
      isBlockWithItemOfType(Schema.CtaBlock.ID),
    );

    if (firstCta) {
      const dup = await duplicateBlockRecord<Schema.CtaBlock>(
        firstCta.node.item,
        repo,
      );
      content.document.children.push({ type: "block", item: dup });
    }

    await client.items.update<Schema.Article>(currentArticle.id, { content });

    console.log("-- AFTER UPDATE --");
    console.log(
      inspectItem(
        await client.items.find<Schema.Article>(currentArticle.id, {
          nested: true,
        }),
      ),
    );
  }
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "RSfdsZbbR7ixGgMBSmcaVA" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "The Future of E-commerce Technology"
  ├ author: "Alex Thompson"
  └ content
    ├ paragraph
    │ ├ span "E-commerce is evolving rapidly with new technologies like "
    │ ├ inlineBlock
    │ │ └ Item "LQlcO4LCTYaOrfj2A705DQ" (item_type: "VGXgXav9SwG5P48frGrFxA")
    │ │   ├ product_name: "AI Shopping Assistant"
    │ │   ├ price: 99.99
    │ │   └ affiliate_url: "https://old-domain.com/product?ref=blog"
    │ └ span " transforming how customers shop online."
    ├ block
    │ └ Item "FqTxaDO8TJmtkoTgDjbK8Q" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
    │   ├ title: "Join the Revolution"
    │   ├ description: "Stay ahead of the curve with our e-commerce insights."
    │   ├ button_text: "Subscribe Now"
    │   └ button_url: "https://old-domain.com/subscribe"
    └ block
      └ Item "JwT0hvgiR4WB0-SO9QG7Ig" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
        ├ title: "E-commerce Innovation Gallery"
        └ images
          ├ [0]
          │ ├ upload_id: "eLVHtrefRUq7qkVMpzG6mQ"
          │ ├ alt: "Modern e-commerce interface"
          │ └ title: "Next-gen Shopping"
          └ [1]
            ├ upload_id: "UFIyIRQFT1GQ7YAG34ub5w"
            ├ alt: "AI-powered recommendations"
            └ title: "Smart Product Discovery"

-- AFTER UPDATE --
└ Item "RSfdsZbbR7ixGgMBSmcaVA" (item_type: "ZV0o9497SsqWxQR8HEQddw")
  ├ title: "The Future of E-commerce Technology"
  ├ author: "Alex Thompson"
  └ content
    ├ paragraph
    │ ├ span "E-commerce is evolving rapidly with new technologies like "
    │ ├ inlineBlock
    │ │ └ Item "LQlcO4LCTYaOrfj2A705DQ" (item_type: "VGXgXav9SwG5P48frGrFxA")
    │ │   ├ product_name: "AI Shopping Assistant"
    │ │   ├ price: 99.99
    │ │   └ affiliate_url: "https://old-domain.com/product?ref=blog&source=article_mention"
    │ └ span " transforming how customers shop online."
    ├ block
    │ └ Item "FqTxaDO8TJmtkoTgDjbK8Q" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
    │   ├ title: "Join the Revolution"
    │   ├ description: "Stay ahead of the curve with our e-commerce insights."
    │   ├ button_text: "Subscribe Now"
    │   └ button_url: "https://new-domain.com/subscribe"
    ├ block
    │ └ Item "JwT0hvgiR4WB0-SO9QG7Ig" (item_type: "I8Q6k-HqQmaZ498WKtvFbg")
    │   ├ title: "E-commerce Innovation Gallery"
    │   └ images
    │     ├ [0]
    │     │ ├ upload_id: "eLVHtrefRUq7qkVMpzG6mQ"
    │     │ ├ alt: "Modern e-commerce interface"
    │     │ └ title: "Next-gen Shopping"
    │     └ [1]
    │       ├ upload_id: "UFIyIRQFT1GQ7YAG34ub5w"
    │       ├ alt: "AI-powered recommendations"
    │       └ title: "Smart Product Discovery"
    └ block
      └ Item "czplSGgiSnizmFZ7gMT-_g" (item_type: "d-CHYg-rShOt3kiL6ZN1yA")
        ├ title: "Join the Revolution"
        ├ description: "Stay ahead of the curve with our e-commerce insights."
        ├ button_text: "Subscribe Now"
        └ button_url: "https://old-domain.com/subscribe"
```

#### Block & node helpers

Curated index of the helpers used above.

###### Block helpers

Used by all three block field types. Full reference: [block-processing-utilities](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#block-processing-utilities).

| Need | Helper |
| --- | --- |
| Build a block (create / edit) | `buildBlockRecord<T>(...)` |
| Clone a block (deep copy, IDs stripped) | `duplicateBlockRecord<T>(block, schemaRepository)` |
| Inspect a record or block (debug) | `inspectItem(record)` |
| Inline narrowing on a block union | `block.__itemTypeId === "..."` |
| Type-guard predicate for `Array#filter` / `Array#find` | `isBlockOfType(id)` |
| Recurse into every block (any depth, any field) | `visitBlocks*` / `mapBlocks*` / `filterBlocks*` / `findAllBlocks*` / `reduceBlocks*` / `someBlocks*` / `everyBlocks*` `InNonLocalizedFieldValue` |

###### Structured-text node helpers

Used only by `structured_text`. Each helper has an `*Async` mirror (`mapNodes` → `mapNodesAsync`) for async callbacks. Full reference: [tree-manipulation-utilities](https://github.com/datocms/structured-text/tree/main/packages/utils#tree-manipulation-utilities).

| Need | Helper |
| --- | --- |
| Narrow a node to a kind | `isParagraph`, `isHeading`, `isSpan`, `isLink`, `isItemLink`, `isInlineItem`, `isBlock`, `isInlineBlock`, `isList`, ... |
| Narrow a block / inline-block node to a specific model in one step | `isBlockWithItemOfType(id)`, `isInlineBlockWithItemOfType(id)` |
| Walk every node (side effect) | `forEachNode` |
| Transform every node (1:1, splat into siblings, or drop) | `mapNodes` |
| Find first / collect every match | `findFirstNode`, `collectNodes` |
| Fold to a single value | `reduceNodes` |
| Short-circuit checks | `someNode`, `everyNode` |
| ASCII-tree debug | `inspect` |

### Updating Localized Fields

**➡️ [Before proceeding, ensure you have read the general guide on Localization](/docs/content-management-api/resources/item.md#localization)**

When you send an update request, the API follows these strict rules.

###### Rule 1: To change a locale value, send the whole set

When you update a translated field, you must provide the **entire object** for that field, including all the languages you want to keep unchanged. You can't just send the one language you're changing.

-   **Correct:** To update the Italian title, you send both English and Italian:
    
    ```json
    {
      "title": {
        "en": "Hello World",
        "it": "Ciao a tutti! (Updated)"
      }
    }
    ```
    
-   **Incorrect:** If you only send the Italian value, the API will assume you want to **delete** the English one!

###### Rule 2: To add/remove a language, send all translated fields

This is the only time you can't just send the one field you're changing. To add or remove a language from an entire record, you **must include all translated fields** in your request. This is to enforce the **Locale Sync Rule** and ensure all fields remain consistent.

-   **Example:** To add French to a blog post that already has a translated `title` and `content`, your request must include both fields with the new `fr` locale.

###### Rule 3: Limited permissions? Only send what you can manage

If your API key only has permission for certain languages (e.g., only English), you must **only include those languages** in your update. The system is smart and will automatically **protect and preserve** the content for the languages you can't access (like Italian or French).

###### Update scenarios at a glance

This table shows what happens in different situations. The key takeaway is that your update payload defines the **new final state** for the languages you are allowed to manage.

| Your Role manages | Record currently Has | Your payload sends | Result |
| --- | --- | --- | --- |
| English | English | English | ✅ English is updated. |
| English, Italian | English | English, Italian | ✅ English is updated. ➕ Italian is **added**. |
| English, Italian | English, Italian | English | ✅ English is updated. ➖ Italian is **removed**. |
| English, Italian | English, Italian | English, Italian | ✅ English is updated. ✅ Italian is updated. |
| Eng, Ita, Fre | English, Italian | English, French | ✅ English is updated. ➖ Italian is **removed**. ➕ French is **added**. |
| English | English, Italian | English | ✅ English is updated. 🛡️ Italian is **preserved**. |
| English, Italian | English, French | English, Italian | ✅ English is updated. 🛡️ French is **preserved**. ➕ Italian is **added**. |
| English, Italian | English, French | Italian | ➖ English is **removed**. 🛡️ French is **preserved**. ➕ Italian is **added**. |

###### Block fields

The rules about localization work in combination with the rules for updating blocks: you use full block objects to create/update and block IDs to leave unchanged, but you do so *within* the object for a specific locale.

<details>
<summary>Example: Updating a block in one locale</summary>

This payload updates the title of an existing block in the `en` locale, while leaving the second English block and all Italian blocks untouched. The `it` locale needs to be included in the payload, or the Italian locale will be deleted!

```json
{
  "content_blocks": {
    "en": [
      {
        "id": "dhVR2HqgRVCTGFi0bWqLqA",
        "type": "item",
        "attributes": { "title": "Updated English Title" }
      },
      "kL9mN3pQrStUvWxYzAbCdE"
    ],
    "it": [
      "dhVR2HqgRVCTGFi_0bWqLqA",
      "kL9mN3pQrStUvWxYzAbCdE"
    ]
  }
}
```

</details>

<details>
<summary>Example: Adding a new block to one locale</summary>

This payload adds a new block to the `it` locale only. The `en` locale needs to be included in the payload, or the Italian locale will be deleted!

```json
{
  "content_blocks": {
    "en": [
      "dhVR2HqgRVCTGFi_0bWqLqA",
      "kL9mN3pQrStUvWxYzAbCdE"
    ],
    "it": [
      "fG8hI1jKlMnOpQrStUvWxY",
      {
        "type": "item",
        "attributes": { "title": "Nuovo Blocco" },
        "relationships": {
          "item_type": { "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" } }
        }
      },
      "dhVR2HqgRVCTGFi0bWqLqA"
    ]
  }
}
```

</details>

<details>
<summary>Example: Adding a new locale</summary>

To add a new locale to an existing record, you must provide values for all localized fields for that new locale, and include existing locales that you want to preserve.

```json
{
  "title": {
    "en": "English Title",
    "fr": "Titre Français",
  },
  "content_blocks": {
    "en": [
      "dhVR2HqgRVCTGFi_0bWqLqA",
      "kL9mN3pQrStUvWxYzAbCdE"
    ],
    "fr": [
      {
        "type": "item",
        "attributes": { "title": "Nouveau Bloc Français" },
        "relationships": {
          "item_type": { "data": { "id": "BxZ9Y2aKQVeTnM4hP8wLpD", "type": "item_type" } }
        }
      }
    ]
  }
}
```

</details>

> [!POSITIVE] One code path for localized and non-localized fields
> Helpers shipped with our JS CMA clients let you skip the "does this field have a locale object or just a value" branching when reading or transforming field values — jump to [Unified locale helpers](/docs/content-management-api/resources/item/update.md#unified-locale-helpers).

###### Example Adding a new locale

Adds the `de` locale to a record that currently has `en` and `it`, in an environment whose locales are `['en', 'it', 'de']`. Per Rule 2 above, the payload must include **every** localized field, with the existing locales preserved alongside the new one. Requires [`all_locales_required`](/docs/content-management-api/resources/item-type.md#object-payload) to be `false` on the model.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Article
 * ├─ author: string
 * ├─ title: string (localized)
 * └─ content: text (localized)
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const item = await client.items.update<Schema.Article>(
    "T4m4tPymSACFzsqbZS65WA",
    {
      title: {
        en: "My title",
        it: "Il mio titolo",
        de: "Mein Titel",
      },
      content: {
        en: "Schema.Article content",
        it: "Contenuto articolo",
        de: "Artikelinhalt",
      },
    },
  );

  console.log(inspectItem(item));
}

run();
```

Returned output

```javascript
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ author: "Stefano Verna"
  ├ title
  │ ├ de: "Mein Titel"
  │ ├ en: "My title"
  │ └ it: "Il mio titolo"
  └ content
    ├ de: "Artikelinhalt"
    ├ en: "Article content"
    └ it: "Contenuto articolo"
```


###### Example Removing an existing locale

If the [`all_locales_required`](/docs/content-management-api/resources/item-type.md#object-payload) option in a model is turned off, then its records do not need all environment's locales to be defined for localized fields, so you're free to add/remove locales during an update operation.

This example demonstrates two approaches for removing a locale from records:

-   **When schema is known:** When you know the exact structure of your models, you can use `ItemTypeDefinition`s to work with full type safety. This approach is ideal for specific, targeted operations.
-   **When schema is unknown:** When you need to work with models dynamically (without knowing their structure ahead of time), you can use [`client.fields.list()`](/docs/content-management-api/resources/field/instances.md) to discover field definitions at runtime. This approach is perfect for bulk operations across multiple models.
    

Both approaches remove the `it` locale by **omitting the unwanted locale** from all localized fields while preserving other locales and non-localized fields.

Code

```javascript
import type { ApiTypes } from "@datocms/cma-client-node";
import {
  buildClient,
  inspectItem,
  isLocalized,
  type LocalizedFieldValue,
} from "@datocms/cma-client-node";
import lodash from "lodash";
import type * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * ├─ cta_block: single_block
 * │  └─ CtaBlock: title, button_text, button_url
 * └─ cover_image: file
 *
 * ArticleCopy
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * ├─ cta_block: single_block
 * │  └─ CtaBlock
 * └─ cover_image: file
 */

// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

async function removeLocaleWhenSchemaIsKnown() {
  // First, fetch the existing record to get current values
  const existingRecord = await client.items.find<Schema.Article>(
    "T4m4tPymSACFzsqbZS65WA",
  );

  console.log("-- BEFORE UPDATE --");
  console.log(inspectItem(existingRecord));

  // Update the record to remove the "it" locale by omitting it from all localized fields
  // Using lodash omit() for clean, readable locale removal
  const updatedRecord = await client.items.update<Schema.Article>(
    "T4m4tPymSACFzsqbZS65WA",
    {
      title: lodash.omit(existingRecord.title, "it"),
      content: lodash.omit(existingRecord.content, "it"),
      // Do not pass non-localized fields (ie. cover_image, cta_block), as we want to keep them unchanged
    },
  );

  console.log("-- AFTER LOCALE REMOVAL --");
  console.log(inspectItem(updatedRecord));
}

// When you don't know the model structure ahead of time,
// you can dynamically load the fields and perform the same operation
async function removeLocaleWhenSchemaIsUnknown() {
  // Get the model fields
  const fields = await client.fields.list("ZV0o9497SsqWxQR8HEQddw");

  // Filter to only localized fields using the isLocalized helper
  const localizedFields = fields.filter(isLocalized);

  // Process all records of this model type
  for await (const record of client.items.listPagedIterator({
    filter: { type: "ZV0o9497SsqWxQR8HEQddw" },
  })) {
    const updatePayload: ApiTypes.ItemUpdateSchema = {};

    // Build update payload by processing each localized field
    for (const field of localizedFields) {
      const fieldValue = record[field.api_key] as LocalizedFieldValue;

      // Remove the "it" locale from each field's localized values
      updatePayload[field.api_key] = lodash.omit(fieldValue, "it");
    }

    // Update the record with the modified locale data
    await client.items.update(record.id, updatePayload);
  }

  console.log("Removed 'it' locale from all records of the model");
}

async function run() {
  // Run both examples
  await removeLocaleWhenSchemaIsKnown();
  await removeLocaleWhenSchemaIsUnknown();
}

run();
```

Returned output

```javascript
-- BEFORE UPDATE --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title
  │ ├ de: "Content-Management verstehen"
  │ ├ en: "Understanding Content Management"
  │ └ it: "Capire la gestione dei contenuti"
  ├ content
  │ ├ de
  │ │ └ paragraph
  │ │   └ span "Ein umfassender Leitfaden für moderne Content-Management-Systeme und ..."
  │ ├ en
  │ │ └ paragraph
  │ │   └ span "A comprehensive guide to modern content management systems and best p..."
  │ └ it
  │   └ paragraph
  │     └ span "Una guida completa ai sistemi di gestione dei contenuti moderni e all..."
  ├ cta_block: "ZPfQFuaqTn2cdoQnPSsu_g"
  └ cover_image
    └ upload_id: "adCusKKeRPO5wtjrIIcGjw"

-- AFTER LOCALE REMOVAL --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title
  │ ├ de: "Content-Management verstehen"
  │ └ en: "Understanding Content Management"
  ├ content
  │ ├ de
  │ │ └ paragraph
  │ │   └ span "Ein umfassender Leitfaden für moderne Content-Management-Systeme und ..."
  │ └ en
  │   └ paragraph
  │     └ span "A comprehensive guide to modern content management systems and best p..."
  ├ cta_block: "ZPfQFuaqTn2cdoQnPSsu_g"
  └ cover_image
    └ upload_id: "adCusKKeRPO5wtjrIIcGjw"

Removed 'it' locale from all records of the model
```


###### Example Copying content from one locale to another

Copies all localized content from `en` to `en-AT` across every model in the project — useful for seeding a new locale, region-specific variations (e.g. UK → Austrian English), or fallback content for incomplete translations.

The script iterates through every model, walks each localized field with [`mapNormalizedFieldValuesAsync`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapnormalizedfieldvalues--mapnormalizedfieldvaluesasync), and recurses into nested blocks with [`mapBlocksInNonLocalizedFieldValue`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapblocksinnonlocalizedfieldvalue) to strip their IDs — that way the target locale gets fresh block instances instead of references to the source ones.

Code

```javascript
import assert from "node:assert";
import type { ApiTypes } from "@datocms/cma-client-node";
import {
  buildClient,
  inspectItem,
  isItemWithOptionalMeta,
  isLocalized,
  type LocalizedFieldValue,
  mapBlocksInNonLocalizedFieldValue,
  mapNormalizedFieldValuesAsync,
  SchemaRepository,
} from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaRepository = new SchemaRepository(client);

  for (const model of await schemaRepository.getAllModels()) {
    const fields = await schemaRepository.getItemTypeFields(model);
    const localizedFields = fields.filter(isLocalized);

    // Iterating across every model discovered at runtime, so no single
    // `<Schema.X>` generic fits — keep `listPagedIterator` untyped and narrow
    // field values dynamically below.
    for await (const record of client.items.listPagedIterator({
      filter: { type: model.api_key },
      version: "current",
      nested: true,
    })) {
      const updatePayload: ApiTypes.ItemUpdateSchema = {};
      let hasChanges = false;

      for (const field of localizedFields) {
        const fieldValueWithNestedBlocks = record[
          field.api_key
        ] as LocalizedFieldValue;

        if (!fieldValueWithNestedBlocks["en"]) {
          continue;
        }

        const newFieldValue = (await mapNormalizedFieldValuesAsync(
          fieldValueWithNestedBlocks,
          field,
          async (_locale, fieldValueForLocale) => {
            return mapBlocksInNonLocalizedFieldValue(
              fieldValueForLocale,
              field.field_type,
              schemaRepository,
              (block) => {
                assert(isItemWithOptionalMeta(block));
                return block.id;
              },
            );
          },
        )) as LocalizedFieldValue;

        // Strip IDs from cloned blocks so en-AT gets fresh instances rather
        // than references to the en blocks.
        newFieldValue["en-AT"] = await mapBlocksInNonLocalizedFieldValue(
          fieldValueWithNestedBlocks["en"],
          field.field_type,
          schemaRepository,
          (block) => {
            assert(isItemWithOptionalMeta(block));
            const { id, ...blockWithoutId } = block;
            return blockWithoutId;
          },
        );

        updatePayload[field.api_key] = newFieldValue;

        hasChanges = true;
      }

      if (hasChanges) {
        console.log("-- EXISTING RECORD --");
        console.log(inspectItem(record));

        console.log("-- UPDATE PAYLOAD --");
        console.log(inspectItem(updatePayload));

        await client.items.update(record.id, updatePayload);

        const nestedRecord = await client.items.find(record.id, {
          nested: true,
        });
        console.log("-- RECORD AFTER UPDATE --");
        console.log(inspectItem(nestedRecord));
      }
    }
  }
}

run();
```

Returned output

```javascript
-- EXISTING RECORD --
└ Item "Bz0dHLjeRuCW10fJl1GF0w" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name
  │ ├ de: "Premium Kabellose Kopfhörer"
  │ └ en: "Premium Wireless Headphones"
  ├ description
  │ ├ de: "Erleben Sie kristallklaren Klang mit unseren hochwertigen kabellosen Kopfhöre..."
  │ └ en: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
  ├ features
  │ ├ de
  │ │ ├ [0] Item "YazSxGr2TlKLJZjJmyhg0Q" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │ │ │ ├ title: "Aktive Geräuschunterdrückung"
  │ │ │ └ description: "Blockieren Sie unerwünschte Geräusche mit unserer fortschrittlichen ANC-Techn..."
  │ │ └ [1] Item "D1Zh8Ff2SaC1CCG67Ic1sQ" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │ │   ├ title: "30 Stunden Akkulaufzeit"
  │ │   └ description: "Ganztägiges Hören mit Schnellladefunktion."
  │ └ en
  │   ├ [0] Item "HqZZmo8sRKuKMfaUZbkNig" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │   │ ├ title: "Active Noise Cancellation"
  │   │ └ description: "Block out unwanted noise with our advanced ANC technology."
  │   └ [1] Item "NKaGQ1AUQZSfHHhL0c3eLA" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │     ├ title: "30-Hour Battery Life"
  │     └ description: "All-day listening with fast charging capabilities."
  └ price: 299.99

-- UPDATE PAYLOAD --
└ Item
  ├ description
  │ ├ de: "Erleben Sie kristallklaren Klang mit unseren hochwertigen kabellosen Kopfhöre..."
  │ ├ en: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
  │ └ en-AT: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
  ├ features
  │ ├ de
  │ │ ├ [0] "YazSxGr2TlKLJZjJmyhg0Q"
  │ │ └ [1] "D1Zh8Ff2SaC1CCG67Ic1sQ"
  │ ├ en
  │ │ ├ [0] "HqZZmo8sRKuKMfaUZbkNig"
  │ │ └ [1] "NKaGQ1AUQZSfHHhL0c3eLA"
  │ └ en-AT
  │   ├ [0] Item (item_type: "T4m4tPymSACFzsqbZS65WA")
  │   │ ├ title: "Active Noise Cancellation"
  │   │ └ description: "Block out unwanted noise with our advanced ANC technology."
  │   └ [1] Item (item_type: "T4m4tPymSACFzsqbZS65WA")
  │     ├ title: "30-Hour Battery Life"
  │     └ description: "All-day listening with fast charging capabilities."
  └ name
    ├ de: "Premium Kabellose Kopfhörer"
    ├ en: "Premium Wireless Headphones"
    └ en-AT: "Premium Wireless Headphones"

-- RECORD AFTER UPDATE --
└ Item "Bz0dHLjeRuCW10fJl1GF0w" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ name
  │ ├ de: "Premium Kabellose Kopfhörer"
  │ ├ en: "Premium Wireless Headphones"
  │ └ en-AT: "Premium Wireless Headphones"
  ├ description
  │ ├ de: "Erleben Sie kristallklaren Klang mit unseren hochwertigen kabellosen Kopfhöre..."
  │ ├ en: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
  │ └ en-AT: "Experience crystal-clear audio with our top-of-the-line wireless headphones f..."
  ├ features
  │ ├ de
  │ │ ├ [0] Item "YazSxGr2TlKLJZjJmyhg0Q" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │ │ │ ├ title: "Aktive Geräuschunterdrückung"
  │ │ │ └ description: "Blockieren Sie unerwünschte Geräusche mit unserer fortschrittlichen ANC-Techn..."
  │ │ └ [1] Item "D1Zh8Ff2SaC1CCG67Ic1sQ" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │ │   ├ title: "30 Stunden Akkulaufzeit"
  │ │   └ description: "Ganztägiges Hören mit Schnellladefunktion."
  │ ├ en
  │ │ ├ [0] Item "HqZZmo8sRKuKMfaUZbkNig" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │ │ │ ├ title: "Active Noise Cancellation"
  │ │ │ └ description: "Block out unwanted noise with our advanced ANC technology."
  │ │ └ [1] Item "NKaGQ1AUQZSfHHhL0c3eLA" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │ │   ├ title: "30-Hour Battery Life"
  │ │   └ description: "All-day listening with fast charging capabilities."
  │ └ en-AT
  │   ├ [0] Item "Txk_qqFJSL6VP3wFzmE_2w" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │   │ ├ title: "Active Noise Cancellation"
  │   │ └ description: "Block out unwanted noise with our advanced ANC technology."
  │   └ [1] Item "eValhxXxTZe-6FW-mwe80g" (item_type: "T4m4tPymSACFzsqbZS65WA")
  │     ├ title: "30-Hour Battery Life"
  │     └ description: "All-day listening with fast charging capabilities."
  └ price: 299.99
```

#### Unified locale helpers

These utilities let you treat localized and non-localized field values with the same code path — no branching on "does this field have a locale object or just a value". Each helper has an `*Async` mirror (`mapNormalizedFieldValues` → `mapNormalizedFieldValuesAsync`) for async callbacks. See the **[full helper reference](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#unified-field-processing-localized--non-localized)** for signatures and examples.

-   `mapNormalizedFieldValues()`: apply a transformation to each locale (or to the single value on non-localized fields)
-   `filterNormalizedFieldValues()`: keep only locales / values matching a predicate
-   `visitNormalizedFieldValues()`: run a side effect for each locale / value
-   `someNormalizedFieldValues()`: `true` if at least one locale / value matches
-   `everyNormalizedFieldValue()`: `true` if all locales / values match
-   `toNormalizedFieldValueEntries()` / `fromNormalizedFieldValueEntries()`: convert to / from a unified `[locale, value][]` shape for iteration

### Bulk block operations

Sometimes, you need to perform mass operations on any block of a specific kind, regardless of where they're embedded in your content structure — whether in Modular Content fields, Single Block fields, or deeply nested within Structured Text documents. In these cases, manually traversing each record and field would be extremely time-consuming and error-prone.

DatoCMS provides powerful utilities that can systematically discover, traverse, and manipulate blocks across your entire content hierarchy. These utilities handle the complexity of localized content, nested structures, and different field types automatically, making what would otherwise be a complex operation straightforward and reliable.

###### Example Edit blocks across all content

Edits specific blocks wherever they're embedded — Modular Content, Single Block, or Structured Text fields, including deeply nested structures and localized content.

To avoid scanning the whole project, the script discovers only the models that can (directly or transitively) embed the target block via [`SchemaRepository.getRawModelsEmbeddingBlocks()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#schemarepository), then walks each record with [`mapNormalizedFieldValuesAsync`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapnormalizedfieldvalues--mapnormalizedfieldvaluesasync) and [`mapBlocksInNonLocalizedFieldValue`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapblocksinnonlocalizedfieldvalue). In this case, every CTA block gets its `style` set to `"primary"` for high-intent copy and `"muted"` otherwise, based on the button text.

Code

```javascript
import assert from "node:assert";
import {
  type ApiTypes,
  buildBlockRecord,
  buildClient,
  inspectItem,
  isItemWithOptionalMeta,
  mapBlocksInNonLocalizedFieldValue,
  mapNormalizedFieldValuesAsync,
  type RawApiTypes,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * │  └─ CtaBlock: title, description, button_text, button_url, style
 * └─ sidebar: rich_text
 *    └─ CtaBlock
 *
 * LandingPage
 * └─ hero_cta: single_block
 *    └─ CtaBlock
 */

// Simplified style decision logic
function computeCtaStyle(text: string | null): "primary" | "muted" {
  if (!text) return "muted";
  return /buy|get started|start free|sign up|upgrade/i.test(text)
    ? "primary"
    : "muted";
}

// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

async function run() {
  const schemaRepository = new SchemaRepository(client);
  const ctaBlockModel = await schemaRepository.getItemTypeById(
    Schema.CtaBlock.ID,
  );

  // 1. Find all models that can embed CTA blocks (directly or indirectly)
  const modelsEmbeddingCtas = await schemaRepository.getModelsEmbeddingBlocks([
    ctaBlockModel,
  ]);

  // 2. Process each model and its records
  for (const model of modelsEmbeddingCtas) {
    console.log(
      `\n📋 Processing records of model: ${model.name} (${model.api_key})`,
    );

    const fields = await schemaRepository.getItemTypeFields(model);

    // This script iterates records across every model discovered at runtime,
    // so there is no single `<Schema.X>` generic that fits — we keep
    // `rawListPagedIterator` untyped here and narrow block values dynamically
    // below. Per-model scripts should always pass the generated marker.
    for await (const record of client.items.rawListPagedIterator({
      filter: { type: model.id },
      version: "current",
      nested: true, // Get full block objects
    })) {
      console.log(`\n--- Processing ${record.id} ---`);
      console.log("BEFORE:");
      console.log(inspectItem(record));

      const updatedAttributes: ApiTypes.ItemUpdateSchema = {};

      // 3. Use mapNormalizedFieldValuesAsync to handle localized/non-localized uniformly
      for (const field of fields) {
        const fieldValue = record.attributes[field.api_key];

        let fieldHasChanges = false;

        const updatedFieldValue = await mapNormalizedFieldValuesAsync(
          fieldValue,
          field,
          async (_locale, normalizedFieldValue) =>
            mapBlocksInNonLocalizedFieldValue(
              normalizedFieldValue,
              field.field_type,
              schemaRepository,
              (block) => {
                assert(isItemWithOptionalMeta(block));

                if (block.__itemTypeId !== Schema.CtaBlock.ID) {
                  return block.id; // Keep non-CTA blocks as is
                }

                // The raw iterator yields records (and their embedded blocks)
                // in the raw API shape; narrow to this specific block model
                // via the ItemInNestedResponse schema indexed by Schema.CtaBlock.
                const ctaBlock =
                  block as RawApiTypes.ItemInNestedResponse<Schema.CtaBlock>;
                const currentStyle = ctaBlock.attributes.style;
                const desiredStyle = computeCtaStyle(
                  ctaBlock.attributes.button_text,
                );

                if (currentStyle !== desiredStyle) {
                  fieldHasChanges = true;

                  // Return an updated block record with new style
                  return buildBlockRecord<Schema.CtaBlock>({
                    id: ctaBlock.id,
                    style: desiredStyle,
                  });
                }

                return block.id; // No change needed
              },
            ),
        );

        if (fieldHasChanges) {
          updatedAttributes[field.api_key] = updatedFieldValue;
        }
      }

      // 4. Update the record if there were changes
      if (Object.keys(updatedAttributes).length > 0) {
        const updatedRecord = await client.items.update(
          record.id,
          updatedAttributes,
        );

        console.log("AFTER:");
        await inspectItemWithNestedBlocks(updatedRecord);
      } else {
        console.log("✨ No changes needed for this record");
      }
    }
  }
}

run();

async function inspectItemWithNestedBlocks(item: ApiTypes.Item) {
  const itemWithNestedBlocks = await client.items.find(item, { nested: true });
  console.log(inspectItem(itemWithNestedBlocks));
}
```

Returned output

```javascript
📋 Processing records of model: Landing Page (landing_page)

--- Processing IwcHsSQ5SSa2LYzpk_Ddjw ---
BEFORE:
└ Item "IwcHsSQ5SSa2LYzpk_Ddjw" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
  └ hero_cta
    └ Item "fnclskI4RG25uLQC92XE5g" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
      ├ title: "Transform Your Business"
      ├ description: "Take the next step forward"
      ├ button_text: "Get started"
      ├ button_url: "/start"
      └ style: "muted"

AFTER:
└ Item "IwcHsSQ5SSa2LYzpk_Ddjw" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
  └ hero_cta
    └ Item "fnclskI4RG25uLQC92XE5g" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
      ├ title: "Transform Your Business"
      ├ description: "Take the next step forward"
      ├ button_text: "Get started"
      ├ button_url: "/start"
      └ style: "primary"

📋 Processing records of model: Article (article)

--- Processing JPplplyPTMKpCbB-wipxeA ---
BEFORE:
└ Item "JPplplyPTMKpCbB-wipxeA" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
  ├ title
  │ ├ en: "Sample Article"
  │ └ it: "Articolo di Esempio"
  ├ content
  │ ├ en
  │ │ ├ paragraph
  │ │ │ └ span "Introduction text"
  │ │ ├ block
  │ │ │ └ Item "BOps1UFgSU-CSm-fRjoqCA" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
  │ │ │   ├ title: "Join Our Platform"
  │ │ │   ├ description: "Start your journey today"
  │ │ │   ├ button_text: "Sign up"
  │ │ │   ├ button_url: "/signup"
  │ │ │   └ style: "muted"
  │ │ └ paragraph
  │ │   └ span "Conclusion text"
  │ └ it
  │   ├ paragraph
  │   │ └ span "Testo introduttivo"
  │   └ block
  │     └ Item "SSVQgIbZS_uoPQWg-qYzWg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
  │       ├ title: "Scopri di Più"
  │       ├ description: "Leggi la nostra guida"
  │       ├ button_text: "Learn more"
  │       ├ button_url: "/guide"
  │       └ style: "primary"
  └ sidebar
    └ [0] Item "f86JBAwxTsasu7J3XxXqJg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
      ├ title: "Special Offer"
      ├ description: "Limited time promotion"
      ├ button_text: "Buy now"
      ├ button_url: "/buy"
      └ style: "muted"

AFTER:
└ Item "JPplplyPTMKpCbB-wipxeA" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
  ├ title
  │ ├ en: "Sample Article"
  │ └ it: "Articolo di Esempio"
  ├ content
  │ ├ en
  │ │ ├ paragraph
  │ │ │ └ span "Introduction text"
  │ │ ├ block
  │ │ │ └ Item "BOps1UFgSU-CSm-fRjoqCA" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
  │ │ │   ├ title: "Join Our Platform"
  │ │ │   ├ description: "Start your journey today"
  │ │ │   ├ button_text: "Sign up"
  │ │ │   ├ button_url: "/signup"
  │ │ │   └ style: "primary"
  │ │ └ paragraph
  │ │   └ span "Conclusion text"
  │ └ it
  │   ├ paragraph
  │   │ └ span "Testo introduttivo"
  │   └ block
  │     └ Item "SSVQgIbZS_uoPQWg-qYzWg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
  │       ├ title: "Scopri di Più"
  │       ├ description: "Leggi la nostra guida"
  │       ├ button_text: "Learn more"
  │       ├ button_url: "/guide"
  │       └ style: "muted"
  └ sidebar
    └ [0] Item "f86JBAwxTsasu7J3XxXqJg" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
      ├ title: "Special Offer"
      ├ description: "Limited time promotion"
      ├ button_text: "Buy now"
      ├ button_url: "/buy"
      └ style: "primary"
```


###### Example Delete blocks across all content

Removes specific blocks wherever they're embedded — Modular Content, Single Block, or Structured Text fields, including deeply nested structures and localized content.

To avoid scanning the whole project, the script discovers only the models that can (directly or transitively) embed the target block via [`SchemaRepository.getRawModelsEmbeddingBlocks()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#schemarepository), then walks each record with [`mapNormalizedFieldValuesAsync`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapnormalizedfieldvalues--mapnormalizedfieldvaluesasync) and [`filterBlocksInNonLocalizedFieldValue`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#filterblocksinnonlocalizedfieldvalue). The removal predicate is an async check (mocked here as an ecommerce SKU lookup), so the same shape works for any external decision.

Code

```javascript
import assert from "node:assert";
import {
  type ApiTypes,
  buildClient,
  filterBlocksInNonLocalizedFieldValue,
  inspectItem,
  isItemWithOptionalMeta,
  mapNormalizedFieldValuesAsync,
  type RawApiTypes,
  SchemaRepository,
} from "@datocms/cma-client-node";
import * as Schema from "./schema.js";

/*
 * Article
 * ├─ title: string (localized)
 * ├─ content: structured_text (localized)
 * │  └─ ProductBlock: sku
 * └─ sidebar: rich_text
 *    └─ ProductBlock
 *
 * ProductPage
 * └─ featured_product: single_block
 *    └─ ProductBlock
 */

// Mock external ecommerce system check
async function isValidSKU(sku: string | null): Promise<boolean> {
  // For demo purposes, consider SKUs starting with "INVALID" as discontinued
  return Boolean(sku && !sku.startsWith("INVALID"));
}

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaRepository = new SchemaRepository(client);
  const productBlockModel = await schemaRepository.getItemTypeById(
    Schema.ProductBlock.ID,
  );

  // 1. Find all models that can embed Product blocks (directly or indirectly)
  const modelsEmbeddingProductBlocks =
    await schemaRepository.getModelsEmbeddingBlocks([productBlockModel]);

  // 2. Process each model and its records
  for (const model of modelsEmbeddingProductBlocks) {
    console.log(
      `\n📋 Processing records of model: ${model.name} (${model.api_key})`,
    );

    const fields = await schemaRepository.getItemTypeFields(model);

    // This script iterates records across every model discovered at runtime,
    // so there is no single `<Schema.X>` generic that fits — we keep
    // `rawListPagedIterator` untyped here and narrow block values dynamically
    // below. Per-model scripts should always pass the generated marker.
    for await (const record of client.items.rawListPagedIterator({
      filter: { type: model.id },
      version: "current",
      nested: true, // Get full block objects
    })) {
      console.log(`\n--- Processing ${record.id} ---`);
      console.log("BEFORE:");
      console.log(inspectItem(record));

      const updatedAttributes: ApiTypes.ItemUpdateSchema = {};

      // 3. Use mapNormalizedFieldValuesAsync to handle localized/non-localized fields uniformly
      for (const field of fields) {
        const fieldValue = record.attributes[field.api_key];

        let fieldHasChanges = false;

        const updatedFieldValue = await mapNormalizedFieldValuesAsync(
          fieldValue,
          field,
          async (_locale, normalizedFieldValue) => {
            // 4. Use filterBlocksInNonLocalizedFieldValue to recursively filter blocks
            const filteredValue = await filterBlocksInNonLocalizedFieldValue(
              normalizedFieldValue,
              field.field_type,
              schemaRepository,
              async (block) => {
                assert(isItemWithOptionalMeta(block));

                // Only check Product blocks
                if (block.__itemTypeId !== Schema.ProductBlock.ID) {
                  return true; // Keep other blocks
                }

                // The raw iterator yields records (and their embedded blocks)
                // in the raw API shape; narrow to this specific block model
                // via the ItemInNestedResponse schema indexed by Schema.ProductBlock.
                const productBlock =
                  block as RawApiTypes.ItemInNestedResponse<Schema.ProductBlock>;

                // Check if the product SKU is still valid in external system
                const isValid = await isValidSKU(productBlock.attributes.sku);

                if (!isValid) {
                  fieldHasChanges = true;
                }

                return isValid;
              },
            );

            return filteredValue;
          },
        );

        if (fieldHasChanges) {
          updatedAttributes[field.api_key] = updatedFieldValue;
        }
      }

      // 5. Update the record if there were changes
      if (Object.keys(updatedAttributes).length > 0) {
        const updatedRecord = await client.items.update(
          record.id,
          updatedAttributes,
        );

        console.log("AFTER:");
        console.log(inspectItem(updatedRecord));
      } else {
        console.log("✨ No changes needed for this record");
      }
    }
  }
}

run();
```

Returned output

```javascript
📋 Processing records of model: Product Page (product_page)

--- Processing RZfnYc3iSya7yYaTxoW0VA ---
BEFORE:
└ Item "RZfnYc3iSya7yYaTxoW0VA" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
  └ featured_product
    └ Item "XayqICFqQEyEqGFypcK07w" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
      └ sku: "INVALID-SKU-004"

AFTER:
└ Item "RZfnYc3iSya7yYaTxoW0VA" (item_type: "KUz2pYAvQvOWqv3dVwVw3w")
  └ featured_product: null

📋 Processing records of model: Article (article)

--- Processing Iaa0ZiZMSjCqeFWfs3JeuQ ---
BEFORE:
└ Item "Iaa0ZiZMSjCqeFWfs3JeuQ" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
  ├ title
  │ ├ en: "Sample Article"
  │ └ it: "Articolo di Esempio"
  ├ content
  │ ├ en
  │ │ ├ paragraph
  │ │ │ └ span "Introduction text"
  │ │ ├ block
  │ │ │ └ Item "Z-xM-VpKTzytfo-5jzSlOw" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
  │ │ │   └ sku: "INVALID-SKU-001"
  │ │ └ paragraph
  │ │   └ span "Conclusion text"
  │ └ it
  │   ├ paragraph
  │   │ └ span "Testo introduttivo"
  │   └ block
  │     └ Item "YoCq3DtzSa2NIbB6ARsiVw" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
  │       └ sku: "VALID-SKU-002"
  └ sidebar
    └ [0] Item "Ulk5GEEoRGSURyyPNSBcow" (item_type: "DC2XVF6BTjGBgQaoaih6Og")
      └ sku: "INVALID-SKU-003"

AFTER:
└ Item "Iaa0ZiZMSjCqeFWfs3JeuQ" (item_type: "ONxSjA4WTWaoNJY2zokUoQ")
  ├ title
  │ ├ en: "Sample Article"
  │ └ it: "Articolo di Esempio"
  ├ content
  │ ├ en
  │ │ ├ paragraph
  │ │ │ └ span "Introduction text"
  │ │ └ paragraph
  │ │   └ span "Conclusion text"
  │ └ it
  │   ├ paragraph
  │   │ └ span "Testo introduttivo"
  │   └ block "YoCq3DtzSa2NIbB6ARsiVw"
  └ sidebar: []
```

### Optimistic Locking

To prevent clients from accidentally overwriting each other's changes, the update endpoint supports optimistic locking. You can include the record's current version number in the `meta` object of your payload.

If the version on the server is newer than the one you provide, the API will reject the update with a `422 STALE_ITEM_VERSION` error, indicating that the record has been modified since you last fetched it.

###### Example Optimistic-locking update operation

Code

```javascript
import { ApiError, type ApiTypes, buildClient } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * Counter
 * ├─ counter: integer
 * └─ description: string
 */

// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

async function run() {
  const itemId = "T4m4tPymSACFzsqbZS65WA";

  console.log("🚀 Starting concurrent updates simulation...\n");

  // Create two competing updates that will run concurrently
  const updateA = updateRecordWithRetry(
    itemId,
    (record) => ({
      counter: (record.counter || 0) + 10,
      description: `Updated by Process A at ${new Date().toISOString()}`,
    }),
    "Process A",
  );

  const updateB = updateRecordWithRetry(
    itemId,
    (record) => ({
      counter: (record.counter || 0) + 5,
      description: `Updated by Process B at ${new Date().toISOString()}`,
    }),
    "Process B",
  );

  try {
    // Run both updates concurrently - one will likely trigger STALE_ITEM_VERSION
    const [resultA, resultB] = await Promise.all([updateA, updateB]);

    console.log("\n✅ Both updates completed successfully!");
    console.log(
      "Final counter value:",
      Math.max(resultA.counter || 0, resultB.counter || 0),
    );

    // Get the final state to see which update won
    const finalRecord = await client.items.find<Schema.Counter>(itemId);
    console.log("\nFinal record state:");
    console.log("- Schema.Counter:", finalRecord.counter);
    console.log("- Description:", finalRecord.description);
    console.log("- Version:", finalRecord.meta.current_version);
  } catch (error) {
    console.error("❌ Unexpected error:", error);
  }
}

async function updateRecordWithRetry(
  itemId: string,
  updateFunction: (record: ApiTypes.Item<Schema.Counter>) => {
    counter?: number;
    description?: string;
  },
  operationName: string,
) {
  // Get the current record
  const record = await client.items.find<Schema.Counter>(itemId);

  console.log(
    `${operationName}: Got record version ${record.meta.current_version}`,
  );

  try {
    // Apply the update with optimistic locking
    const updatedRecord = await client.items.update<Schema.Counter>(itemId, {
      ...updateFunction(record),
      meta: { current_version: record.meta.current_version },
    });

    console.log(
      `${operationName}: Update successful! New version: ${updatedRecord.meta.current_version}`,
    );
    return updatedRecord;
  } catch (e) {
    // Handle STALE_ITEM_VERSION error by retrying
    if (e instanceof ApiError && e.findError("STALE_ITEM_VERSION")) {
      console.log(
        `${operationName}: ❌ STALE_ITEM_VERSION detected! Record was modified by another client.`,
      );
      console.log(`${operationName}: 🔄 Retrying with fresh data...`);

      // Recursive retry with exponential backoff
      await new Promise((resolve) =>
        setTimeout(resolve, Math.random() * 100 + 50),
      );
      return updateRecordWithRetry(itemId, updateFunction, operationName);
    }

    throw e;
  }
}

run();
```

Returned output

```javascript
🚀 Starting concurrent updates simulation...

Process A: Got record version L80WevK_R6Gh6ijMyW0AkQ
Process B: Got record version L80WevK_R6Gh6ijMyW0AkQ
Process A: Update successful! New version: QHXWBDr7S9ipQo6_JjQpAg
Process B: ❌ STALE_ITEM_VERSION detected! Record was modified by another client.
Process B: 🔄 Retrying with fresh data...
Process B: Got record version QHXWBDr7S9ipQo6_JjQpAg
Process B: Update successful! New version: A4VMeUdtRT26NWd07g6pzw

✅ Both updates completed successfully!
Final counter value: 15

Final record state:
- Counter: 15
- Description: Updated by Process B at 2025-09-26T08:25:53.088Z
- Version: A4VMeUdtRT26NWd07g6pzw
```

## Body parameters

**`meta.created_at`**

- Optional
- Type: string

Date of creation

**`meta.first_published_at`**

- Optional
- Type: null, string

Date of first publication

**`meta.current_version`**

- Optional
- Type: string
- Example: `"4234"`

The ID of the current record version (for optimistic locking, see the example)

**`meta.stage`**

- Optional
- Type: string, null

The new stage to move the record to

**`item_type`**

- Optional
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

The record's model

**`creator`**

- Optional
- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"access_token"\>](https://www-draft.datocms.com/docs/content-management-api/resources/access_token.md), [ResourceLinkage\<"user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/user.md), [ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

The entity (account/collaborator/access token/sso user) who created the record

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

---

# Content Management API — Referenced records

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/references.md

List all records that link to a specific record

## Query parameters

**`nested`**

- Type: boolean

For Modular Content, Structured Text and Single Block fields, return full payload for nested blocks instead of IDs

**`version`**

- Type: null, enum
- Example: `"current"`

Retrieve only the selected type of version that is linked to the record; current, published or both

<details>
<summary>Show enum values</summary>

**`current`**

Return records that in their latest version available link to the record

**`published`**

Return records that in their published version link to the record

**`published-or-current`**

Return records that either in their published version or in their latest version available link to the record

</details>

## Returns

Returns an array of resource objects of type [item](/docs/content-management-api/resources/item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "hWl-mnkWRYmMCSTq4z_piQ";

  const items = await client.items.references(itemId);

  for (const item of items) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(item);
  }
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Retrieve a record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/self.md

> [!PROTIP] 📚 New to DatoCMS records?
> Begin by reading the [Introduction to records](/docs/content-management-api/resources/item.md) guide to familiarize yourself with field types, API response modes, and the concepts of block manipulation!

To retrieve a single record, send a GET request to the `/items/:id` endpoint.

## Response modes: Regular vs. Nested

The `GET /items/:id` endpoint, just like the [List all records](/docs/content-management-api/resources/item/instances.md) endpoint, supports two different response modes that control how block fields are returned in the JSON payload. You can switch between them using the nested query parameter.

-   **Regular mode (default):** This is the most efficient mode for listing records. Any block fields (like Modular Content) will contain an array of **block IDs**, not the full block content. This keeps the response size small and fast.
-   **Nested mode (`nested=true`):** This mode returns the complete content for any block fields. Instead of just IDs, the API will return full **block objects**, including all their attributes. This is useful when you need to display the blocks' content immediately without making additional API calls, or to read existing content and then make an update.
    

###### Example Regular mode (default)

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "hWl-mnkWRYmMCSTq4z_piQ";

  const item = await client.items.find(itemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(item);
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```


###### Example Nested mode

Sometimes, you may wish to fetch a record that has embedded blocks inside Modular Content, Single Block or Structured Text fields.

By default, those nested blocks are returned as block IDs (ie. `"dhVR2HqgRVCTGFi_0bWqLqA"`), but if you add the `nested: true` query parameter, we'll embed the blocks content *inline* for you.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * └─ sections: rich_text
 *    └─ HeroBlock: headline
 */

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Regular mode — the `sections` field contains bare block IDs.
  const regular = await client.items.find<Schema.BlogPost>(
    "FEzWmQhjQgeHsCrUtvlEMw",
  );
  console.log("-- Regular mode --");
  console.log(inspectItem(regular));

  // Nested mode — the same `sections` field contains the full block
  // objects inline, ready to be read or re-sent to the API.
  const nested = await client.items.find<Schema.BlogPost>(
    "FEzWmQhjQgeHsCrUtvlEMw",
    {
      nested: true,
    },
  );
  console.log("-- Nested mode --");
  console.log(inspectItem(nested));
}

run();
```

Returned output

```javascript
{
  id: "FEzWmQhjQgeHsCrUtvlEMw",
  type: "item",
  structured_text_field: {
    schema: "dast",
    document: {
      children: [
        {
          item: {
            type: "item",
            attributes: {
              button_label: "Example button",
              button_url: "https://www.example.com",
            },
            relationships: {
              item_type: {
                data: { id: "SkVjHJSGR5CyK16E8TfJxg", type: "item_type" },
              },
            },
            id: "ahxSnFQEQ02K3TjttWAg-Q",
          },
          type: "block",
        },
        {
          item: {
            type: "item",
            attributes: {
              nested_structured_text_field: {
                schema: "dast",
                document: {
                  children: [
                    {
                      children: [
                        { type: "span", value: "This is a " },
                        {
                          marks: ["emphasis"],
                          type: "span",
                          value: "nested",
                        },
                        {
                          type: "span",
                          value: " structured text block inside the parent structured text field.",
                        },
                      ],
                      type: "paragraph",
                    },
                    {
                      item: {
                        type: "item",
                        attributes: {
                          button_label: "And this is a button inside the nested structured text block",
                          button_url: "https://www.example2.com",
                        },
                        relationships: {
                          item_type: {
                            data: {
                              id: "SkVjHJSGR5CyK16E8TfJxg",
                              type: "item_type",
                            },
                          },
                        },
                        id: "CGqwjPDsTHKGFy1IbC0RAQ",
                      },
                      type: "block",
                    },
                  ],
                  type: "root",
                },
              },
            },
            relationships: {
              item_type: {
                data: { id: "Ty4S40cbQH6_VMNnGdd9KA", type: "item_type" },
              },
            },
            id: "AppHB06oRBm-er3oooL_LA",
          },
          type: "block",
        },
      ],
      type: "root",
    },
  },
  item_type: { id: "UVa_hHEBSeefLEUnwoQFig", type: "item_type" },
  creator: { id: "104280", type: "account" },
  meta: {
    created_at: "2024-03-13T17:01:19.243+00:00",
    updated_at: "2024-03-13T17:14:17.444+00:00",
    published_at: "2024-03-13T17:14:17.597+00:00",
    publication_scheduled_at: null,
    unpublishing_scheduled_at: null,
    first_published_at: "2024-03-13T17:01:19.326+00:00",
    is_valid: true,
    is_current_version_valid: true,
    is_published_version_valid: true,
    status: "published",
    current_version: "DLtyHZ2MTDqYMg7g5mgYEw",
    stage: null,
  },
}
```

## TypeScript typing

Reading a record without typed schemas means every attribute comes back as `unknown`, and the IDE can't help you navigate it. The single biggest lever you have is passing a generated `Schema.X` marker as the generic on `items.find`. TypeScript then knows the exact shape of the returned record — its field names, types, and block structures — so reads are typed end-to-end:

```ts
import * as Schema from "./schema";

const record = await client.items.find<Schema.Article>("record-id");
record.title; // typed, not unknown
```

For the exact type of a specific field on the returned record (to annotate a helper or intermediate variable), index `ApiTypes.Item<Schema.Article>["field_api_key"]` (or `ApiTypes.ItemInNestedResponse<Schema.Article>["field_api_key"]` when fetching with `nested: true`). See the [full TypeScript guide](https://www.datocms.com/cma-ts-schema.md) for how to generate `schema.ts` and the complete pattern.

## Query parameters

**`nested`**

- Type: boolean

For Modular Content, Structured Text and Single Block fields. If set, returns full payload for nested blocks instead of IDs

**`version`**

- Type: string
- Example: `"published"`

Whether you want the currently published versions (`published`) of your records, or the latest available (`current`, default)

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

---

# Content Management API — Delete a record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/destroy.md

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "hWl-mnkWRYmMCSTq4z_piQ";

  const item = await client.items.destroy(itemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(item);
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Publish a record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/publish.md

When the [draft/published system](/docs/general-concepts/draft-published.md) is enabled for a model, records will remain in a *Draft* status until they are *Published*.

When publishing a record, you can choose to either publish the whole record, or just some of its locales / non-localized content. This is similar to how the "Publish" dropdown button in the UI works.

###### Example Publish entire record (all locales & non-localized content)

This is the default behavior when you don't provide a request body.

This will publish the entire record, including all its localized and non-localized fields.

Do not include a request body at all — not even an empty object `{}`.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * └─ slug: slug
 */

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "T4m4tPymSACFzsqbZS65WA";

  // The record starts as a Draft. The attributes are the same before and
  // after publishing; what changes is the record's publication state, which
  // lives in `meta` (status / published_at / first_published_at).
  const beforePublish = await client.items.find<Schema.BlogPost>(itemId);
  console.log("-- BEFORE PUBLISH --");
  console.log(`meta.status: "${beforePublish.meta.status}"`);
  console.log(`meta.published_at: ${beforePublish.meta.published_at}`);
  console.log(inspectItem(beforePublish));

  const publishedRecord = await client.items.publish<Schema.BlogPost>(itemId);

  console.log("-- AFTER PUBLISH --");
  console.log(`meta.status: "${publishedRecord.meta.status}"`);
  console.log(`meta.published_at: ${publishedRecord.meta.published_at}`);
  console.log(inspectItem(publishedRecord));
}

run();
```

Returned output

```javascript
-- BEFORE PUBLISH --
meta.status: "draft"
meta.published_at: null
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "My first blog post!"
  └ slug: "my-first-blog-post"

-- AFTER PUBLISH --
meta.status: "published"
meta.published_at: 2026-04-23T10:02:27.599+01:00
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "My first blog post!"
  └ slug: "my-first-blog-post"
```


###### Example Selective publishing (only specified locales or non-localized content)

Selective publishing is used when you don't want to publish the entire record. Instead, you can publish a combination of:

-   Zero or more [specified locales](http://localhost:3000/product-updates/get-locales-list-from-graphql)
-   And/or all of this record's non-localized fields

In this example, we will only publish the `en` locale. The `it` and `es` versions of `localized_title` will not be published, and will retain their previously published titles. `non_localized_field` will also keep its previously published value.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string (localized)
 * └─ slug: slug
 */

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "T4m4tPymSACFzsqbZS65WA";

  // The record is already Published with content in both locales, but its
  // `en` title has newer draft changes that haven't been published yet.
  // Pass `version: "published"` to read the latest *published* version so
  // we can see what is actually live.
  const publishedBefore = await client.items.find<Schema.BlogPost>(itemId, {
    version: "published",
  });
  console.log("-- Published version BEFORE --");
  console.log(inspectItem(publishedBefore));

  // Selectively publish only the `en` locale — leave `it` untouched and
  // do not re-publish non-localized fields (the slug).
  await client.items.publish<Schema.BlogPost>(itemId, {
    content_in_locales: ["en"],
    non_localized_content: false,
  });

  // Re-read the published version: `en` is now the updated text; `it` and
  // `slug` are unchanged.
  const publishedAfter = await client.items.find<Schema.BlogPost>(itemId, {
    version: "published",
  });
  console.log("-- Published version AFTER --");
  console.log(inspectItem(publishedAfter));
}

run();
```

Returned output

```javascript
-- Published version BEFORE --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title
  │ ├ en: "My first blog post!"
  │ └ it: "Il mio primo post!"
  └ slug: "my-first-blog-post"

-- Published version AFTER --
└ Item "T4m4tPymSACFzsqbZS65WA" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title
  │ ├ en: "My first blog post! (updated)"
  │ └ it: "Il mio primo post!"
  └ slug: "my-first-blog-post"
```

## TypeScript typing

Publishing a record without typed schemas gives you back a record whose attributes are all `unknown` — any downstream code that reads from it is fighting TypeScript. The single biggest lever you have is passing a generated `Schema.X` marker as the generic on `items.publish`. TypeScript then knows the exact shape of the returned record — its field names, types, and block structures — so reads are typed end-to-end:

```ts
import * as Schema from "./schema";

const record = await client.items.publish<Schema.Article>("record-id");
record.title; // typed, not unknown
```

For the exact type of a specific field on the returned record (to annotate a helper or intermediate variable), index `ApiTypes.Item<Schema.Article>["field_api_key"]`. See the [full TypeScript guide](https://www.datocms.com/cma-ts-schema.md) for how to generate `schema.ts` and the complete pattern.

## Query parameters

**`recursive`**

- Type: boolean

When `recursive` is `true`, if the record belongs to a [tree-like collection](https://www.datocms.com/docs/content-modelling/trees), and any of the parent records aren't published, those parent records will published as well. When `recursive` is `false` or not specified, an `UNPUBLISHED_PARENT` error will occur in such cases.

## Body parameters

For this endpoint, the body is not required and can be entirely omitted.

**`content_in_locales`**

- Required
- Type: Array\<string\>
- Examples: `["en"]`, `["en", "it"]`

Array of [valid locale codes in this project](/product-updates/get-locales-list-from-graphql) to publish.

**`non_localized_content`**

- Required
- Type: boolean

Whether non-localized content will be published

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

## Other examples

###### Example Known issue with legacy client: 'id' is not a permitted key

If you're using the the older, now-deprecated [`datocms-client`](https://www.npmjs.com/package/datocms-client) instead of the current [`@datocms/cma-client`](https://www.npmjs.com/package/@datocms/cma-client), there is a known issue with the `item.publish()` method. It will return an error like:

```json
{
    "data": [
        {
            "id": "abcdef",
            "type": "api_error",
            "attributes": {
                "code": "INVALID_FORMAT",
                "details": {
                    "messages": [
                        "#/data: failed schema #/definitions/item/links/13/schema/properties/data: \"id\" is not a permitted key."
                    ]
                }
            }
        }
    ]
}
```

The workaround is to add `{serializeRequest: false}` as the third parameter of that method, like:

```js
await client.item.publish(
    "1234567890", // record ID
    {}, // body
    {}, // query string
    { serializeRequest: false } // this is the actual workaround
  );
```

This tells the deprecated client to skip some of its internal serialization rules (which used to work, but no longer) and instead just send the raw syntax that you provide.

While this should allow that method to continue working for the time being, it is important that you upgrade to the modern client as soon as possible. As of 2024, the old client has been deprecated for more than 2 years and will not receive any further updates. It is possible that this method and others will further break over time, possibly impacting production workflows.

Code

```javascript
import { SiteClient } from "datocms-client";

async function run() {
  const client = new SiteClient(process.env.DATOCMS_API_TOKEN);

  const itemId = "T4m4tPymSACFzsqbZS65WA";

  const publishedRecord = await client.items.publish(
    itemId,
    {}, // body
    {}, // query string
    { serializeRequest: false }, // this is the actual workaround
  );

  console.log(publishedRecord);
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Unpublish a record

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/unpublish.md

In a model where the [draft/published system](/docs/general-concepts/draft-published.md) is enabled, *Published* records can subsequently be **Unpublished** in order to return them to *Draft* status.

When unpublishing a record, you can choose to either unpublish the whole record, or just some of its locales, similar to how the "Unpublish" dropdown button in the UI sidebar works.

###### Example Unpublish entire record (all locales)

This is the default behavior when you don't provide a request body.

This will unpublish the entire record, including all its localizations.

Do not include a request body at all — not even an empty object `{}`.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string
 * └─ slug: slug
 */

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "fyq6ADkeTL6Ryk7s98xmHw";

  // The record starts as Published. The attributes are the same before and
  // after unpublishing; what changes is the record's publication state,
  // which lives in `meta` (status / published_at).
  const beforeUnpublish = await client.items.find<Schema.BlogPost>(itemId);
  console.log("-- BEFORE UNPUBLISH --");
  console.log(`meta.status: "${beforeUnpublish.meta.status}"`);
  console.log(`meta.published_at: ${beforeUnpublish.meta.published_at}`);
  console.log(inspectItem(beforeUnpublish));

  const unpublishedItem = await client.items.unpublish<Schema.BlogPost>(itemId);

  console.log("-- AFTER UNPUBLISH --");
  console.log(`meta.status: "${unpublishedItem.meta.status}"`);
  console.log(`meta.published_at: ${unpublishedItem.meta.published_at}`);
  console.log(inspectItem(unpublishedItem));
}

run();
```

Returned output

```javascript
-- BEFORE UNPUBLISH --
meta.status: "published"
meta.published_at: 2026-04-23T10:02:26.737+01:00
└ Item "fyq6ADkeTL6Ryk7s98xmHw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "My first blog post!"
  └ slug: "my-first-blog-post"

-- AFTER UNPUBLISH --
meta.status: "draft"
meta.published_at: null
└ Item "fyq6ADkeTL6Ryk7s98xmHw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title: "My first blog post!"
  └ slug: "my-first-blog-post"
```


###### Example Selective unpublishing (unpublish specified locales only, keeping others published)

Selective unpublishing is used when you only want to unpublish certain localizations instead of the whole record.

**Please note**: You can only unpublish locales that are currently published within a specific record. If you try to unpublish a record's locale that is already unpublished (i.e. in draft state) or doesn't exist in the record at all (even if the project has that locale), you will get a `VALIDATION_INVALID` error on the `content_in_locales` field.

Code

```javascript
import { buildClient, inspectItem } from "@datocms/cma-client-node";
import type * as Schema from "./schema.js";

/*
 * BlogPost
 * ├─ title: string (localized)
 * └─ slug: slug
 */

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "fyq6ADkeTL6Ryk7s98xmHw";

  // Read the current published version so we can compare it to the new
  // published version after the selective unpublish.
  const publishedBefore = await client.items.find<Schema.BlogPost>(itemId, {
    version: "published",
  });
  console.log("-- Published version BEFORE --");
  console.log(inspectItem(publishedBefore));

  // Unpublish only the `it` locale — the `en` locale and the non-localized
  // fields stay published.
  await client.items.unpublish<Schema.BlogPost>(itemId, {
    content_in_locales: ["it"],
  });

  // Re-read the published version: the `it` translation is gone; `en` and
  // `slug` are unchanged.
  const publishedAfter = await client.items.find<Schema.BlogPost>(itemId, {
    version: "published",
  });
  console.log("-- Published version AFTER --");
  console.log(inspectItem(publishedAfter));
}

run();
```

Returned output

```javascript
-- Published version BEFORE --
└ Item "fyq6ADkeTL6Ryk7s98xmHw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title
  │ ├ en: "My first blog post!"
  │ └ it: "Il mio primo post!"
  └ slug: "my-first-blog-post"

-- Published version AFTER --
└ Item "fyq6ADkeTL6Ryk7s98xmHw" (item_type: "UZyfjdBES8y2W2ruMEHSoA")
  ├ title
  │ └ en: "My first blog post!"
  └ slug: "my-first-blog-post"
```

## TypeScript typing

Unpublishing a record without typed schemas gives you back a record whose attributes are all `unknown` — any downstream code that reads from it is fighting TypeScript. The single biggest lever you have is passing a generated `Schema.X` marker as the generic on `items.unpublish`. TypeScript then knows the exact shape of the returned record — its field names, types, and block structures — so reads are typed end-to-end:

```ts
import * as Schema from "./schema";

const record = await client.items.unpublish<Schema.Article>("record-id");
record.title; // typed, not unknown
```

For the exact type of a specific field on the returned record (to annotate a helper or intermediate variable), index `ApiTypes.Item<Schema.Article>["field_api_key"]`. See the [full TypeScript guide](https://www.datocms.com/cma-ts-schema.md) for how to generate `schema.ts` and the complete pattern.

## Query parameters

**`recursive`**

- Type: boolean

When `recursive` is `true`, if the record belongs to a [tree-like collection](https://www.datocms.com/docs/content-modelling/trees), and any of the children records are published, those children records will unpublished as well. When `recursive` is `false` or not specified, a `PUBLISHED_CHILDREN` error will occur in such cases.

## Body parameters

For this endpoint, the body is not required and can be entirely omitted.

**`content_in_locales`**

- Required
- Type: Array\<string\>
- Examples: `["en"]`, `["en", "it"]`

Array of locales to publish. They must be currently published in this record. To unpublish all locales, do NOT use this parameter, but instead unpublish the entire record by leaving the body blank (see example above).

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

---

# Content Management API — Publish items in bulk

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/bulk_publish.md

## Body parameters

**`items`**

- Required
- Type: Array<[ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md)>

Records to publish (a maximum of 200 records are allowed per request)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const item = await client.items.bulkPublish({
    items: [{ type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" }],
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(item);
}

run();
```

Returned output

```javascript
[]
```

---

# Content Management API — Unpublish items in bulk

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/bulk_unpublish.md

## Body parameters

**`items`**

- Required
- Type: Array<[ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md)>

Records to unpublish (a maximum of 200 records are allowed per request)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const item = await client.items.bulkUnpublish({
    items: [{ type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" }],
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(item);
}

run();
```

Returned output

```javascript
[]
```

---

# Content Management API — Destroy items in bulk

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/bulk_destroy.md

## Body parameters

**`items`**

- Required
- Type: Array<[ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md)>

Records to delete (a maximum of 200 records are allowed per request)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const item = await client.items.bulkDestroy({
    items: [{ type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" }],
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(item);
}

run();
```

Returned output

```javascript
[]
```

---

# Content Management API — Move items to stage in bulk

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item/bulk_move_to_stage.md

## Body parameters

**`stage`**

- Required
- Type: string
- Example: `"in_review"`

Stage to be moved to

**`items`**

- Required
- Type: Array<[ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md)>

Records to move (a maximum of 200 records are allowed per request)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const item = await client.items.bulkMoveToStage({
    stage: "in_review",
    items: [{ type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" }],
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(item);
}

run();
```

Returned output

```javascript
[]
```

---

# Content Management API — Scheduled publication

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/scheduled-publication.md

You can create scheduled publication to publish records in the future

## Object payload

**`id`**

- Type: string
- Example: `"34"`

ID of scheduled_publication

**`type`**

- Type: string

Must be exactly `"scheduled_publication"`.

**`publication_scheduled_at`**

- Type: date-time
- Example: `"2025-02-10T11:03:42Z"`

The future date for the publication

**`selective_publication`**

- Type: null, object

Specifies which content should be published. If null, the whole record will be published.

<details>
<summary>Show object format</summary>

**`content_in_locales`**

- Type: Array\<string\>

List of locales whose content will be published

**`non_localized_content`**

- Type: boolean

Whether the non-localized content has to be published or not

</details>

**`item`**

- Type: [ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md)

Item

---

# Content Management API — Create a new scheduled publication

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/scheduled-publication/create.md

## Body parameters

**`publication_scheduled_at`**

- Required
- Type: date-time
- Example: `"2025-02-10T11:03:42Z"`

The future date for the publication

**`selective_publication`**

- Optional
- Type: null, object

Specifies which content should be published. If null, the whole record will be published.

<details>
<summary>Show object format</summary>

**`content_in_locales`**

- Required
- Type: Array\<string\>

List of locales whose content will be published

**`non_localized_content`**

- Required
- Type: boolean

Whether the non-localized content has to be published or not

</details>

## Returns

Returns a resource object of type [scheduled\_publication](/docs/content-management-api/resources/scheduled-publication.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "34";

  const scheduledPublication = await client.scheduledPublication.create(
    itemId,
    { publication_scheduled_at: "2025-02-10T11:03:42Z" },
  );

  // Check the 'Returned output' tab for the result ☝️
  console.log(scheduledPublication);
}

run();
```

Returned output

```javascript
{
  id: "34",
  publication_scheduled_at: "2025-02-10T11:03:42Z",
  selective_publication: {
    content_in_locales: ["en"],
    non_localized_content: true,
  },
  item: { type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" },
}
```

---

# Content Management API — Delete a scheduled publication

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/scheduled-publication/destroy.md

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "34";

  const scheduledPublication =
    await client.scheduledPublication.destroy(itemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(scheduledPublication);
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Scheduled unpublishing

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing.md

You can create a scheduled unpublishing to unpublish records in the future

## Object payload

**`id`**

- Type: string
- Example: `"34"`

ID of scheduled_unpublishing

**`type`**

- Type: string

Must be exactly `"scheduled_unpublishing"`.

**`unpublishing_scheduled_at`**

- Type: date-time
- Example: `"2025-02-10T11:03:42Z"`

The future date for the unpublishing

**`content_in_locales`**

- Type: null, Array\<string\>

List of locales whose content will be unpublished, or nil if the whole record needs to be unpublished

**`item`**

- Type: [ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md)

Item

---

# Content Management API — Create a new scheduled unpublishing

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/create.md

## Body parameters

**`unpublishing_scheduled_at`**

- Required
- Type: date-time
- Example: `"2025-02-10T11:03:42Z"`

The future date for the unpublishing

**`content_in_locales`**

- Optional
- Type: null, Array\<string\>

List of locales whose content will be unpublished, or nil if the whole record needs to be unpublished

## Returns

Returns a resource object of type [scheduled\_unpublishing](/docs/content-management-api/resources/scheduled-unpublishing.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "34";

  const scheduledUnpublishing = await client.scheduledUnpublishing.create(
    itemId,
    { unpublishing_scheduled_at: "2025-02-10T11:03:42Z" },
  );

  // Check the 'Returned output' tab for the result ☝️
  console.log(scheduledUnpublishing);
}

run();
```

Returned output

```javascript
{
  id: "34",
  unpublishing_scheduled_at: "2025-02-10T11:03:42Z",
  content_in_locales: ["en"],
  item: { type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" },
}
```

---

# Content Management API — Delete a scheduled unpublishing

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/destroy.md

## Returns

Returns a resource object of type [item](/docs/content-management-api/resources/item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "34";

  const scheduledUnpublishing =
    await client.scheduledUnpublishing.destroy(itemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(scheduledUnpublishing);
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Upload

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload.md

Each media object you upload to the Media Area of your DatoCMS project is represented as an `upload` entity.

## Object payload

**`id`**

- Type: string
- Example: `"q0VNpiNQSkG6z0lif_O1zg"`

RFC 4122 UUID of upload expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"upload"`.

**`size`**

- Type: integer
- Example: `444`

size of the upload

**`width`**

- Type: null, integer
- Example: `30`

Width of image

**`height`**

- Type: null, integer
- Example: `30`

Height of image

**`path`**

- Type: string
- Example: `"/45/1496845848-digital-cats.jpg"`

Upload path

**`basename`**

- Type: string
- Example: `"digital-cats"`

Upload basename

**`filename`**

- Type: string
- Example: `"digital-cats.jpg"`

Upload filename

**`url`**

- Type: string
- Example: `"https://www.datocms-assets.com/45/1496845848-digital-cats.jpg"`

Upload URL

**`format`**

- Type: string, null
- Example: `"jpg"`

Format

**`author`**

- Type: string, null
- Example: `"Mark Smith"`

Author

**`copyright`**

- Type: string, null
- Example: `"2020 DatoCMS"`

Copyright

**`notes`**

- Type: string, null
- Example: `"Nyan the cat"`

Notes

**`md5`**

- Type: string
- Example: `"873c296d0f2b7ee569f2d7ddaebc0d33"`

The MD5 hash of the asset

**`duration`**

- Type: integer, null
- Example: `62`

Seconds of duration for the video

**`frame_rate`**

- Type: integer, null
- Example: `30`

Frame rate (FPS) for the video

**`blurhash`**

- Type: string, null
- Example: `"LEHV6nWB2yk8pyo0adR*.7kCMdnj"`

Blurhash for the asset

**`thumbhash`**

- Type: string, null
- Example: `"UhqCDQIkrHOfVG8wBa2v39z7CXeqZWFLdg=="`

Base64 encoded ThumbHash for the asset

**`mux_playback_id`**

- Type: string, null
- Example: `"a1B2c3D4e5F6g7H8i9"`

Public Mux playback ID. Used with stream.mux.com to create the source URL for a video player.

**`mux_mp4_highest_res`**

- Type: enum, null
- Example: `"high"`

Maximum quality of MP4 rendition available

<details>
<summary>Show enum values</summary>

**`high`**

**`medium`**

**`low`**

</details>

**`default_field_metadata`**

- Type: object

Per-asset default metadata applied when no record-level overrides are present. `alt`, `title`, and `custom_data` are objects keyed by locale; `focal_point` (image assets) and `poster_time` (video assets) are a single value per asset. See [non-localized focal points](https://www.datocms.com/product-updates/non-localized-focal-points) for more info.

Example:

```json
{
  alt: { en: "this is the default alternate text" },
  title: { en: "this is the default title" },
  custom_data: { en: { foo: "bar" } },
  focal_point: { x: 0.5, y: 0.5 },
  poster_time: 12.5,
}
```

<details>
<summary>Show object format</summary>

**`alt`**

- Type: object
- Example: `{ en: "Alternate text" }`

Alternate text per locale

**`title`**

- Type: object
- Example: `{ en: "Title" }`

Title per locale

**`custom_data`**

- Type: object
- Example: `{ en: { add_watermark: true } }`

Object with arbitrary metadata, per locale

**`focal_point`**

- Type: object, null

Focal point (only for image assets)

<details>
<summary>Show object format</summary>

**`x`**

- Type: number
- Example: `0.5`

Horizontal position expressed as float between 0 and 1

**`y`**

- Type: number
- Example: `0.5`

Vertical position expressed as float between 0 and 1

</details>

**`poster_time`**

- Type: number, null
- Example: `12.5`

Poster time in seconds (only for video assets). Float seconds into the video used to generate the thumbnail; null uses Mux's default (middle of the video)

</details>

**`is_image`**

- Type: boolean

Is this upload an image?

**`created_at`**

- Type: null, date-time

Date of upload

**`updated_at`**

- Type: null, date-time

Date of last update

**`mime_type`**

- Type: null, string
- Example: `"image/jpeg"`

Mime type of upload

**`tags`**

- Type: Array\<string\>
- Example: `["cats"]`

Tags

**`smart_tags`**

- Type: Array\<string\>
- Example: `["robot-cats"]`

Smart tags

**`exif_info`**

- Type: object

Exif information

Example:

```json
{
  iso: 10000,
  model: "ILCE-7",
  flash_mode: 16,
  focal_length: 35,
  exposure_time: 0.0166667,
}
```

**`colors`**

- Type: Array\<object\>

Dominant colors of the image

Example:

```json
[
  { red: 206, green: 203, blue: 167, alpha: 255 },
  { red: 158, green: 163, blue: 93, alpha: 255 },
]
```

<details>
<summary>Show objects format inside array</summary>

**`red`**

- Type: integer
- Example: `115`

Red value (from 0 to 255)

**`green`**

- Type: integer
- Example: `133`

Green value (from 0 to 255)

**`blue`**

- Type: integer
- Example: `27`

Blue value (from 0 to 255)

**`alpha`**

- Type: integer
- Example: `255`

Alpha value (from 0 to 255)

</details>

**`meta.antivirus`**

- Type: object

Antivirus scan information

<details>
<summary>Show object format</summary>

**`status`**

- Type: enum
- Example: `"clean"`

Antivirus scan status of the asset

<details>
<summary>Show enum values</summary>

**`pending`**

**`clean`**

**`infected`**

**`failed`**

**`skipped`**

</details>

**`checked_at`**

- Type: date-time, null

Date of the last antivirus scan

**`threat_name`**

- Type: string, null

Name of the threat detected by the antivirus scan, if any

</details>

**`creator`**

- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"access_token"\>](https://www-draft.datocms.com/docs/content-management-api/resources/access_token.md), [ResourceLinkage\<"user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/user.md), [ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

The entity (account/collaborator/access token) who created the asset

**`upload_collection`**

- Type: [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md), null

Upload collection to which the asset belongs

---

# Content Management API — Create a new upload

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/create.md

The DatoCMS clients provide numerous methods for users to upload resources. The method you choose can be influenced by different aspects like the platform you're using (such as Node.js or a browser) and where the resource is coming from — like a local file, a remote URL, or a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) or [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) obtained from `<input type="file" />` elements.

###### Example Node.js: Create an upload from a local file

This example shows how to add assets to the Media Area by uploading a local file.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Create upload resource from a local file
  const upload2 = await client.uploads.createFromLocalFile({
    // local path of the file to upload
    localPath: "./image.png",
    // if you want, you can specify a different base name for the uploaded file
    filename: "different-image-name.png",
    // skip the upload and return an existing resource if it's already present in the Media Area:
    skipCreationIfAlreadyExists: true,
    // specify some additional metadata to the upload resource
    author: "New author!",
    copyright: "New copyright",
    default_field_metadata: {
      alt: { en: "New default alt" },
      title: { en: "New default title" },
      custom_data: {
        en: {
          watermark: true,
        },
      },
      focal_point: {
        x: 0.3,
        y: 0.6,
      },
    },
  });

  console.log(upload2);
}

run();
```

Returned output

```javascript
const result = {
  id: "4124",
  size: 444,
  width: 30,
  height: 30,
  path: "/45/1496845848-different-image-name.png",
  basename: "image",
  url: "https://www.datocms-assets.com/45/1496845848-different-image-name.png",
  format: "jpg",
  author: "New author!",
  copyright: "New copyright",
  notes: null,
  default_field_metadata: {
    alt: { en: "new default alt" },
    title: { en: "new default title" },
    custom_data: {
      en: {
        watermark: true,
      },
    },
    focal_point: {
      x: 0.3,
      y: 0.6,
    },
  },
  is_image: true,
  tags: [],
};
```


###### Example Node.js: Create an upload from a remote URL

Here's a demonstration of how you can uploading an asset from a remote location, accessible through a URL.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Create upload resource from a remote URL
  const upload = await client.uploads.createFromUrl({
    // remote URL to upload
    url: "https://example.com/image.png",
    // if you want, you can specify a different base name for the uploaded file
    filename: "different-image-name.png",
    // skip the upload and return an existing resource if it's already present in the Media Area:
    skipCreationIfAlreadyExists: true,
    // specify some additional metadata to the upload resource
    author: "New author!",
    copyright: "New copyright",
  });

  console.log(upload);
}

run();
```

Returned output

```javascript
const result = {
  id: "4124",
  size: 444,
  width: 30,
  height: 30,
  path: "/45/1496845848-different-image-name.png",
  basename: "image",
  url: "https://www.datocms-assets.com/45/1496845848-different-image-name.png",
  format: "jpg",
  author: "New author!",
  copyright: "New copyright",
  notes: null,
  default_field_metadata: {
    alt: { en: "new default alt" },
    title: { en: "new default title" },
    custom_data: {
      en: {
        watermark: true,
      },
    },
    focal_point: {
      x: 0.3,
      y: 0.6,
    },
  },
  is_image: true,
  tags: [],
};
```


###### Example Browser: Create an upload from a File or Blob object

This example shows how to add assets to the Media Area from the browser, starting from a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) or [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object.

**Important!** Make sure to use the `@datocms/cma-client-browser` package, or the `client.uploads.createFromFileOrBlob()` method won't be available!

Code

```javascript
import { buildClient } from "@datocms/cma-client-browser";

// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

function createUpload(file: File) {
  return client.uploads.createFromFileOrBlob({
    // File object to upload
    fileOrBlob: file,
    // if you want, you can specify a different base name for the uploaded file
    filename: "different-image-name.png",
    // specify some additional metadata to the upload resource
    author: "New author!",
    copyright: "New copyright",
    default_field_metadata: {
      alt: { en: "New default alt" },
      title: { en: "New default title" },
      custom_data: {
        en: {
          watermark: true,
        },
      },
      focal_point: {
        x: 0.3,
        y: 0.6,
      },
    },
  });
}

const fileInput = document.querySelector(
  'input[type="file"]',
) as HTMLInputElement;

fileInput.addEventListener("change", async (event) => {
  const target = event.target as HTMLInputElement;
  const files = target.files;
  if (files) {
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      if (file) {
        createUpload(file).then((upload) => console.log(upload));
      }
    }
  }
});
```

Returned output

```javascript
const response = {
  id: "4124",
  size: 444,
  width: 30,
  height: 30,
  path: "/45/1496845848-different-image-name.png",
  basename: "image",
  url: "https://www.datocms-assets.com/45/1496845848-different-image-name.png",
  format: "jpg",
  author: "New author!",
  copyright: "New copyright",
  notes: null,
  default_field_metadata: {
    alt: { en: "new default alt" },
    title: { en: "new default title" },
    custom_data: {
      en: {
        watermark: true,
      },
    },
    focal_point: {
      x: 0.3,
      y: 0.6,
    },
  },
  is_image: true,
  tags: [],
};
```


###### Example Monitoring the progress

Regardless of the upload method, you can always get information about the operation's progress by listening to the events that hit the `onProgress` callback.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  await client.uploads.createFromUrl({
    url: "https://example.com/image.png",
    onProgress: ({ type, ...rest }) => {
      // info.type can be one of the following:
      //
      // * DOWNLOADING_FILE: client is downloading the asset from the specified URL
      // * REQUESTING_UPLOAD_URL: client is requesting permission to upload the asset to the DatoCMS CDN
      // * UPLOADING_FILE: client is uploading the asset
      // * CREATING_UPLOAD_OBJECT: client is finalizing the creation of the upload resource
      //
      // The rest of the information depends on the type of notification

      console.log(type, rest);
    },
  });
}

run();
```

Returned output

```javascript
DOWNLOADING_FILE { url: "https://example.com/image.png", progress: 20 }
DOWNLOADING_FILE { url: "https://example.com/image.png", progress: 90 }
DOWNLOADING_FILE { url: "https://example.com/image.png", progress: 100 }

REQUESTING_UPLOAD_URL { filaname: 'image.png' }

UPLOADING_FILE { progress: 10 }
UPLOADING_FILE { progress: 80 }
UPLOADING_FILE { progress: 100 }

CREATING_UPLOAD_OBJECT undefined
```

Each available method yields a cancellable promise, granting the ability to halt a currently running upload operation.

###### Example Cancelling an in-progress upload

It is possible to cancel an upload operation by calling the `.cancel()` method on the promise returned by one of the upload creation methods (`createFromUrl()`, `createFromLocalFile()` in NodeJS, `createFromFileOrBlob()` in browser):

Code

```javascript
import {
  type ApiTypes,
  buildClient,
  type CancelablePromise,
  CanceledPromiseError,
} from "@datocms/cma-client-browser";

// Make sure the API token has access to the CMA, and is stored securely
const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

let cancelablePromise: CancelablePromise<ApiTypes.Upload> | null = null;

const cancelButton = document.querySelector("button")!;

cancelButton.addEventListener("click", () => {
  if (cancelablePromise) {
    cancelablePromise.cancel();
  }
});

const fileInput = document.querySelector(
  'input[type="file"]',
) as HTMLInputElement;
fileInput.addEventListener("change", async (event) => {
  const target = event.target as HTMLInputElement;
  const files = target.files;

  if (files?.[0]) {
    cancelablePromise = client.uploads.createFromFileOrBlob({
      fileOrBlob: files[0],
    });

    cancelablePromise
      .then((upload) => {
        cancelablePromise = null;
        console.log(upload);
      })
      .catch((e) => {
        if (e instanceof CanceledPromiseError) {
          console.log("User canceled the upload process!");
        } else {
          throw e;
        }
      });
  }
});
```

Returned output

```javascript
{
  id: "q0VNpiNQSkG6z0lif_O1zg",
  size: 444,
  width: 30,
  height: 30,
  path: "/45/1496845848-digital-cats.jpg",
  basename: "digital-cats",
  filename: "digital-cats.jpg",
  url: "https://www.datocms-assets.com/45/1496845848-digital-cats.jpg",
  format: "jpg",
  author: "Mark Smith",
  copyright: "2020 DatoCMS",
  notes: "Nyan the cat",
  md5: "873c296d0f2b7ee569f2d7ddaebc0d33",
  duration: 62,
  frame_rate: 30,
  blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
  thumbhash: "UhqCDQIkrHOfVG8wBa2v39z7CXeqZWFLdg==",
  mux_playback_id: "a1B2c3D4e5F6g7H8i9",
  mux_mp4_highest_res: "high",
  default_field_metadata: {
    alt: { en: "this is the default alternate text" },
    title: { en: "this is the default title" },
    custom_data: { en: { foo: "bar" } },
    focal_point: { x: 0.5, y: 0.5 },
    poster_time: 12.5,
  },
  is_image: true,
  created_at: "2020-04-21T07:57:11.124Z",
  updated_at: "2020-04-21T07:57:11.124Z",
  mime_type: "image/jpeg",
  tags: ["cats"],
  smart_tags: ["robot-cats"],
  exif_info: {
    iso: 10000,
    model: "ILCE-7",
    flash_mode: 16,
    focal_length: 35,
    exposure_time: 0.0166667,
  },
  colors: [
    { red: 206, green: 203, blue: 167, alpha: 255 },
    { red: 158, green: 163, blue: 93, alpha: 255 },
  ],
  meta: {
    antivirus: {
      status: "clean",
      checked_at: "2020-04-21T07:57:11.124Z",
      threat_name: null,
    },
  },
  creator: { type: "account", id: "312" },
  upload_collection: {
    type: "upload_collection",
    id: "uinr2zfqQLeCo_1O0-ao-Q",
  },
}
```

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"q0VNpiNQSkG6z0lif_O1zg"`

RFC 4122 UUID of upload expressed in URL-safe base64 format

**`path`**

- Required
- Type: string
- Example: `"/45/1496845848-digital-cats.jpg"`

Upload path

**`copyright`**

- Optional
- Type: string, null
- Example: `"2020 DatoCMS"`

Copyright

**`author`**

- Optional
- Type: string, null
- Example: `"Mark Smith"`

Author

**`notes`**

- Optional
- Type: string, null
- Example: `"Nyan the cat"`

Notes

**`default_field_metadata`**

- Optional
- Type: object

Patch the asset's default metadata. Send any subset of `alt`/`title`/`custom_data`/`focal_point`/`poster_time` — missing keys preserve their stored values. See the response shape for the full structure and per-key semantics.

<details>
<summary>Show object format</summary>

**`alt`**

- Optional
- Type: object
- Example: `{ en: "Alternate text" }`

Alternate text per locale

**`title`**

- Optional
- Type: object
- Example: `{ en: "Title" }`

Title per locale

**`custom_data`**

- Optional
- Type: object
- Example: `{ en: { add_watermark: true } }`

Object with arbitrary metadata, per locale

**`focal_point`**

- Optional
- Type: object, null

Focal point (only for image assets)

<details>
<summary>Show object format</summary>

**`x`**

- Required
- Type: number
- Example: `0.5`

Horizontal position expressed as float between 0 and 1

**`y`**

- Required
- Type: number
- Example: `0.5`

Vertical position expressed as float between 0 and 1

</details>

**`poster_time`**

- Optional
- Type: number, null
- Example: `12.5`

Poster time in seconds (only for video assets). Float seconds into the video used to generate the thumbnail; null uses Mux's default (middle of the video)

</details>

**`tags`**

- Optional
- Type: Array\<string\>
- Example: `["cats"]`

Tags

**`upload_collection`**

- Optional
- Type: [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md), null

Upload collection to which the asset belongs

## Returns

Returns a resource object of type [upload](/docs/content-management-api/resources/upload.md)

---

# Content Management API — List all uploads

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/instances.md

To retrieve a collection of uploads, send a GET request to the `/uploads` endpoint. The collection is [paginated](/docs/content-management-api/pagination.md), so make sure to iterate over all the pages if you need every record in the collection!

The following table contains the list of all the possible arguments, along with their type, description and examples values.

Pro tip: in case of any doubts you can always inspect the network calls that the CMS interface is doing, as it's using the Content Management API as well!

## Query parameters

**`filter`**

- Type: object

Attributes to filter uploads

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"12,31"`

IDs to fetch, comma separated

**`query`**

- Type: string
- Example: `"foobar"`

Textual query to match. If `locale` is defined, search within that locale. Otherwise environment's main locale will be used.

**`fields`**

- Type: object
- Example: `{ type: { eq: "image" }, size: { gt: 5000000 } }`

Same as [GraphQL API uploads filters](/docs/content-delivery-api/filtering-uploads). Use snake_case for fields names. If `locale` is defined, search within that locale. Otherwise environment's main locale will be used.

</details>

**`locale`**

- Type: string
- Example: `"it"`

When `filter[query]` or `field[fields]` is defined, filter by this locale. Default: environment's main locale

**`order_by`**

- Type: string
- Example: `"_created_at_DESC,size_ASC"`

Fields used to order results. Format: `<field_name>_<DIRECTION(ASC|DESC)>`. You can pass multiple comma separated rules.

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer
- Example: `200`

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 30, maximum is 500)

</details>

## Returns

Returns an array of resource objects of type [upload](/docs/content-management-api/resources/upload.md)

## Other examples

###### Example Fetching one page of results vs. the whole collection

The `client.uploads.list()` method returns a single page of records, while if you need to iterate over **every** resource in the collection (and not just the first page of results), you can use the `client.uploads.listPagedIterator()` method with an [async iteration statement](https://github.com/tc39/proposal-async-iteration#the-async-iteration-statement-for-await-of), which automatically handles pagination for you.

All the details on how to use `list()` and `listPagedIterator()` are outlined [on this page](/docs/content-management-api/pagination.md#paged-iterators).

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // iterates over every page of results
  for await (const upload of client.uploads.listPagedIterator()) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(upload);
  }
}

run();
```


###### Example Fetching a filtered list of uploads

You can retrieve a list of uploads filtered by a set of conditions. There are different options and you can combine multiple filters together.

In this example we are filtering by type and size. In particular, we are searching for images bigger than 5MB.

The filtering options are the same as the [GraphQL API uploads filters](/docs/content-delivery-api/filtering-uploads.md). So please check there all the options.

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploads = await client.uploads.list({
    filter: {
      fields: {
        type: {
          eq: "image",
        },
        size: {
          gt: 5000000,
        },
      },
    },
  });

  console.log(uploads);
}

run();
```

---

# Content Management API — Retrieve an upload

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/self.md

## Returns

Returns a resource object of type [upload](/docs/content-management-api/resources/upload.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "q0VNpiNQSkG6z0lif_O1zg";

  const upload = await client.uploads.find(uploadId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(upload);
}

run();
```

Returned output

```javascript
{
  id: "q0VNpiNQSkG6z0lif_O1zg",
  size: 444,
  width: 30,
  height: 30,
  path: "/45/1496845848-digital-cats.jpg",
  basename: "digital-cats",
  filename: "digital-cats.jpg",
  url: "https://www.datocms-assets.com/45/1496845848-digital-cats.jpg",
  format: "jpg",
  author: "Mark Smith",
  copyright: "2020 DatoCMS",
  notes: "Nyan the cat",
  md5: "873c296d0f2b7ee569f2d7ddaebc0d33",
  duration: 62,
  frame_rate: 30,
  blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
  thumbhash: "UhqCDQIkrHOfVG8wBa2v39z7CXeqZWFLdg==",
  mux_playback_id: "a1B2c3D4e5F6g7H8i9",
  mux_mp4_highest_res: "high",
  default_field_metadata: {
    alt: { en: "this is the default alternate text" },
    title: { en: "this is the default title" },
    custom_data: { en: { foo: "bar" } },
    focal_point: { x: 0.5, y: 0.5 },
    poster_time: 12.5,
  },
  is_image: true,
  created_at: "2020-04-21T07:57:11.124Z",
  updated_at: "2020-04-21T07:57:11.124Z",
  mime_type: "image/jpeg",
  tags: ["cats"],
  smart_tags: ["robot-cats"],
  exif_info: {
    iso: 10000,
    model: "ILCE-7",
    flash_mode: 16,
    focal_length: 35,
    exposure_time: 0.0166667,
  },
  colors: [
    { red: 206, green: 203, blue: 167, alpha: 255 },
    { red: 158, green: 163, blue: 93, alpha: 255 },
  ],
  meta: {
    antivirus: {
      status: "clean",
      checked_at: "2020-04-21T07:57:11.124Z",
      threat_name: null,
    },
  },
  creator: { type: "account", id: "312" },
  upload_collection: {
    type: "upload_collection",
    id: "uinr2zfqQLeCo_1O0-ao-Q",
  },
}
```

---

# Content Management API — Delete an upload

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/destroy.md

## Returns

Returns a resource object of type [upload](/docs/content-management-api/resources/upload.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "q0VNpiNQSkG6z0lif_O1zg";

  const upload = await client.uploads.destroy(uploadId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(upload);
}

run();
```

Returned output

```javascript
{
  id: "q0VNpiNQSkG6z0lif_O1zg",
  size: 444,
  width: 30,
  height: 30,
  path: "/45/1496845848-digital-cats.jpg",
  basename: "digital-cats",
  filename: "digital-cats.jpg",
  url: "https://www.datocms-assets.com/45/1496845848-digital-cats.jpg",
  format: "jpg",
  author: "Mark Smith",
  copyright: "2020 DatoCMS",
  notes: "Nyan the cat",
  md5: "873c296d0f2b7ee569f2d7ddaebc0d33",
  duration: 62,
  frame_rate: 30,
  blurhash: "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
  thumbhash: "UhqCDQIkrHOfVG8wBa2v39z7CXeqZWFLdg==",
  mux_playback_id: "a1B2c3D4e5F6g7H8i9",
  mux_mp4_highest_res: "high",
  default_field_metadata: {
    alt: { en: "this is the default alternate text" },
    title: { en: "this is the default title" },
    custom_data: { en: { foo: "bar" } },
    focal_point: { x: 0.5, y: 0.5 },
    poster_time: 12.5,
  },
  is_image: true,
  created_at: "2020-04-21T07:57:11.124Z",
  updated_at: "2020-04-21T07:57:11.124Z",
  mime_type: "image/jpeg",
  tags: ["cats"],
  smart_tags: ["robot-cats"],
  exif_info: {
    iso: 10000,
    model: "ILCE-7",
    flash_mode: 16,
    focal_length: 35,
    exposure_time: 0.0166667,
  },
  colors: [
    { red: 206, green: 203, blue: 167, alpha: 255 },
    { red: 158, green: 163, blue: 93, alpha: 255 },
  ],
  meta: {
    antivirus: {
      status: "clean",
      checked_at: "2020-04-21T07:57:11.124Z",
      threat_name: null,
    },
  },
  creator: { type: "account", id: "312" },
  upload_collection: {
    type: "upload_collection",
    id: "uinr2zfqQLeCo_1O0-ao-Q",
  },
}
```

---

# Content Management API — Update an upload

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/update.md

Depending on the attributes that you pass, you can use this endpoint to:

-   **Update regular attributes** like `author`, `notes`, `copyright`, `default_field_metadata`, etc.;
-   **Rename the asset** by passing a different `basename` attribute;
-   **Upload a new version of the asset** by passing a different `path` attribute;

Just like `POST /uploads` endpoint, an asyncronous job ID might be returned instead of the regular response. See the [Create a new upload](/docs/content-management-api/resources/upload.md#create) section for more details.

**We strongly recommend to use our JS or Ruby client to upload new assets**, as they provide helper methods that take care of all the details for you.

## Query parameters

**`replace_strategy`**

- Type: enum
- Example: `"keep_url"`

Strategy to use when replacing the asset file. If not specified, a new URL will be generated.

<details>
<summary>Show enum values</summary>

**`create_new_url`**

Generate a new URL for the asset (default behavior)

**`keep_url`**

Maintain the same URL by overwriting the file at the existing path

</details>

## Body parameters

**`path`**

- Optional
- Type: string
- Example: `"/45/1496845848-digital-cats.jpg"`

Upload path

**`basename`**

- Optional
- Type: string
- Example: `"digital-cats"`

Upload basename

**`copyright`**

- Optional
- Type: string, null
- Example: `"2020 DatoCMS"`

Copyright

**`author`**

- Optional
- Type: string, null
- Example: `"Mark Smith"`

Author

**`notes`**

- Optional
- Type: string, null
- Example: `"Nyan the cat"`

Notes

**`tags`**

- Optional
- Type: Array\<string\>
- Example: `["cats"]`

Tags

**`default_field_metadata`**

- Optional
- Type: object

Patch the asset's default metadata. Send any subset of `alt`/`title`/`custom_data`/`focal_point`/`poster_time` — missing keys preserve their stored values. See the response shape for the full structure and per-key semantics.

<details>
<summary>Show object format</summary>

**`alt`**

- Optional
- Type: object
- Example: `{ en: "Alternate text" }`

Alternate text per locale

**`title`**

- Optional
- Type: object
- Example: `{ en: "Title" }`

Title per locale

**`custom_data`**

- Optional
- Type: object
- Example: `{ en: { add_watermark: true } }`

Object with arbitrary metadata, per locale

**`focal_point`**

- Optional
- Type: object, null

Focal point (only for image assets)

<details>
<summary>Show object format</summary>

**`x`**

- Required
- Type: number
- Example: `0.5`

Horizontal position expressed as float between 0 and 1

**`y`**

- Required
- Type: number
- Example: `0.5`

Vertical position expressed as float between 0 and 1

</details>

**`poster_time`**

- Optional
- Type: number, null
- Example: `12.5`

Poster time in seconds (only for video assets). Float seconds into the video used to generate the thumbnail; null uses Mux's default (middle of the video)

</details>

**`creator`**

- Optional
- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"access_token"\>](https://www-draft.datocms.com/docs/content-management-api/resources/access_token.md), [ResourceLinkage\<"user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/user.md), [ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

The entity (account/collaborator/access token) who created the asset

**`upload_collection`**

- Optional
- Type: [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md), null

Upload collection to which the asset belongs

## Returns

Returns a resource object of type [upload](/docs/content-management-api/resources/upload.md)

## Other examples

###### Example Update asset attributes

This example demonstrates how to update the metadata attributes of an existing asset without changing the underlying file.

You can update fields like:

-   **`author`**: The creator or photographer of the asset
-   **`copyright`**: Copyright information for the asset
-   **`default_field_metadata`**: Default metadata for the asset. `alt`, `title`, and `custom_data` are keyed by locale, while `focal_point` (images) and `poster_time` (videos) are single, non-localized values

The `default_field_metadata` object allows you to set defaults that will be used when the asset is referenced in records, unless overridden at the record level.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "FWYwxZZ1SUG1ReKlzdDAEA";

  const updatedUpload = await client.uploads.update(uploadId, {
    author: "New author!",
    copyright: "New copyright",
    default_field_metadata: {
      alt: { en: "new default alt" },
      title: { en: "new default title" },
      custom_data: { en: {} },
      focal_point: {
        x: 0.3,
        y: 0.6,
      },
    },
  });

  console.log({
    id: updatedUpload.id,
    author: updatedUpload.author,
    copyright: updatedUpload.copyright,
    default_field_metadata: updatedUpload.default_field_metadata,
  });
}

run();
```

Returned output

```javascript
{
  id: 'FWYwxZZ1SUG1ReKlzdDAEA',
  author: 'New author!',
  copyright: 'New copyright',
  default_field_metadata: {
    alt: { en: 'new default alt' },
    title: { en: 'new default title' },
    custom_data: { en: {} },
    focal_point: { x: 0.3, y: 0.6 },
    poster_time: null
  }
}
```


###### Example Replace the asset file

This example demonstrates how to replace the file associated with an existing upload while keeping the same upload ID.

When replacing an asset, you have two options:

**Create new URL** (default): The new asset is available immediately with a fresh URL. The old URL will be purged from cache and will disappear after complete propagation. Use this when you need immediate availability of the new file.

**Keep the original URL**: Existing links continue to work automatically, but changes take 5-10 minutes to appear everywhere due to CDN and browser cache propagation. Some users may temporarily see the old version. Use this when you have many existing references and want to avoid updating URLs.

> [!WARNING] Important considerations for keep_url
> The `keep_url` option is only available when the new file has the same format/extension as the original (e.g., replacing a `.jpg` with another `.jpg`/`.jpeg`). If the formats differ, you must use the default `create_new_url` strategy.
> 
> Also note that since different upload entities across environments can share the same asset URL, using `keep_url` will automatically update all uploads that reference this asset path. This ensures consistency across environments but means the change affects more than just the current upload.

The `uploadLocalFileAndReturnPath()` helper handles:

-   Requesting upload permissions from DatoCMS
-   Uploading the file to the storage bucket
-   Returning the path to use in the update call

Code

```javascript
import {
  buildClient,
  uploadLocalFileAndReturnPath,
} from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Fetch the original uploads to show their URLs before replacement
  const original1 = await client.uploads.find("bDb8xUJIRaGteyJYldfKsA");
  const original2 = await client.uploads.find("II2mHvSkQESUl-2401HZgg");

  console.log("Before replacement:");
  console.log("  Upload 1 URL:", original1.url);
  console.log("  Upload 2 URL:", original2.url);

  // Upload the new file and get the path
  const newFilePath = await uploadLocalFileAndReturnPath(
    client,
    "./new-image.jpg",
  );

  // ============================================================
  // Option 1: Create new URL (default)
  // ============================================================
  // The new asset is available immediately with a fresh URL.
  // Use this when you need immediate availability.

  const uploadWithNewUrl = await client.uploads.update(
    "bDb8xUJIRaGteyJYldfKsA",
    { path: newFilePath },
  );

  // ============================================================
  // Option 2: Keep the original URL
  // ============================================================
  // Existing links work automatically, but changes take 5-10 minutes
  // to propagate. Use this when you have many existing references.

  const uploadKeepingUrl = await client.uploads.update(
    "II2mHvSkQESUl-2401HZgg",
    { path: newFilePath },
    { replace_strategy: "keep_url" },
  );

  console.log("\nAfter replacement:");
  console.log("  Option 1 (new URL):", uploadWithNewUrl.url);
  console.log("  Option 2 (keep URL):", uploadKeepingUrl.url);
}

run();
```

Returned output

```javascript
Before replacement:
  Upload 1 URL: https://www.datocms-assets.com/190734/1768292263-old-image.jpg
  Upload 2 URL: https://www.datocms-assets.com/190734/1768292270-old-image.jpg

After replacement:
  Option 1 (new URL): https://www.datocms-assets.com/190734/1768292279-new-image.jpg
  Option 2 (keep URL): https://www.datocms-assets.com/190734/1768292270-old-image.jpg
```


###### Example Rename the file

This example demonstrates how to rename an uploaded file in the CDN by changing its `basename`.

This is particularly useful for SEO purposes, as the filename becomes part of the asset's URL. Renaming allows you to use descriptive, keyword-rich filenames without needing to re-upload the file.

The `basename` is the filename without the extension. For example, setting `basename` to `"premium-headphones"` for a `.jpg` file would result in a URL ending in `premium-headphones.jpg`.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "ey4EL5V0QYCdODxbbUj2cQ";

  // Rename the uploaded file in the CDN (for SEO purposes)
  const updatedUpload = await client.uploads.update(uploadId, {
    basename: "this-will-be-the-new-file-basename",
  });

  console.log({
    id: updatedUpload.id,
    basename: updatedUpload.basename,
    path: updatedUpload.path,
    url: updatedUpload.url,
  });
}

run();
```

Returned output

```javascript
{
  id: 'ey4EL5V0QYCdODxbbUj2cQ',
  basename: 'this-will-be-the-new-file-basename',
  path: '/190729/1768291467-this-will-be-the-new-file-basename.jpeg',
  url: 'https://www.datocms-assets.com/190729/1768291467-this-will-be-the-new-file-basename.jpeg'
}
```

---

# Content Management API — Referenced records

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/references.md

Retrieve all records that are linked to this upload

## Query parameters

**`nested`**

- Type: boolean

For Modular Content, Structured Text and Single Block fields, return full payload for nested blocks instead of IDs

**`version`**

- Type: null, enum
- Example: `"current"`

Retrieve only the selected type of version that is linked to the upload; current, published or both

<details>
<summary>Show enum values</summary>

**`current`**

Return records that are linked to the upload in their latest version available

**`published`**

Return records that are linked to the upload in their published version

**`published-or-current`**

Return records that are linked to the upload either in their published version or in their latest version available

</details>

## Returns

Returns an array of resource objects of type [item](/docs/content-management-api/resources/item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "q0VNpiNQSkG6z0lif_O1zg";

  const uploads = await client.uploads.references(uploadId);

  for (const upload of uploads) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(upload);
  }
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Add tags to assets in bulk

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/bulk_tag.md

## Body parameters

**`tags`**

- Required
- Type: Array\<string\>
- Example: `["cats"]`

The tags to add to the assets

**`uploads`**

- Required
- Type: Array<[ResourceLinkage\<"upload"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload.md)>

Assets to tag

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const upload = await client.uploads.bulkTag({
    tags: ["cats"],
    uploads: [{ type: "upload", id: "q0VNpiNQSkG6z0lif_O1zg" }],
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(upload);
}

run();
```

Returned output

```javascript
[]
```

---

# Content Management API — Put assets into a collection in bulk

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/bulk_set_upload_collection.md

## Body parameters

**`uploads`**

- Required
- Type: Array<[ResourceLinkage\<"upload"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload.md)>

Assets to assign to the collection

**`upload_collection`**

- Required
- Type: null, [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md)

Asset collection to put uploads into

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const upload = await client.uploads.bulkSetUploadCollection({
    uploads: [{ type: "upload", id: "q0VNpiNQSkG6z0lif_O1zg" }],
    upload_collection: null,
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(upload);
}

run();
```

Returned output

```javascript
[]
```

---

# Content Management API — Destroy uploads

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload/bulk_destroy.md

Delete assets in bulk

## Body parameters

**`uploads`**

- Required
- Type: Array<[ResourceLinkage\<"upload"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload.md)>

Assets to delete

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const upload = await client.uploads.bulkDestroy({
    uploads: [{ type: "upload", id: "q0VNpiNQSkG6z0lif_O1zg" }],
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(upload);
}

run();
```

Returned output

```javascript
[]
```

---

# Content Management API — Site

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site.md

A site represents a specific DatoCMS administrative area

## Object payload

**`id`**

- Type: string
- Example: `"155"`

ID of site

**`type`**

- Type: string

Must be exactly `"site"`.

**`domain`**

- Type: string, null
- Example: `"admin.my-awesome-website.com"`

Administrative area custom domain

**`favicon`**

- Type: string, null
- Example: `"123"`

The upload id for the favicon

**`global_seo`**

- Type: object, null

Specifies default global settings

<details>
<summary>Show object format</summary>

**`site_name`**

- Type: string
- Example: `"My Awesome Website"`

Site name, used in social sharing

**`fallback_seo`**

- Type: object

<details>
<summary>Show object format</summary>

**`title`**

- Type: string
- Example: `"Default meta title"`

**`description`**

- Type: string
- Example: `"Default meta description"`

**`image`**

- Type: null, string
- Example: `"123"`

The id of the image

**`twitter_card`**

- Type: null, enum
- Example: `"summary_large_image"`

Determines how a Twitter link preview is shown

<details>
<summary>Show enum values</summary>

**`summary`**

Twitter summary card

**`summary_large_image`**

Twitter summary card with large image

</details>

</details>

**`title_suffix`**

- Type: null, string
- Example: `" - My Awesome Website"`

Title meta tag suffix

**`facebook_page_url`**

- Type: null, string
- Example: `"http://facebook.com/awesomewebsite"`

URL of facebook page

**`twitter_account`**

- Type: null, string
- Example: `"@awesomewebsite"`

Twitter account associated to website

</details>

**`google_maps_api_token`**

- Type: string, null
- Example: `"xxxxxxxxxxxxx"`

Google API Key to be used by the LatLon field editor. Only the DatoCMS interface uses it: it returns `null` for any other caller.

**`imgix_host`**

- Type: string, null
- Example: `"www.datocms-assets.com"`

Imgix host

**`internal_domain`**

- Type: string, null
- Example: `"my-website.admin.datocms.com"`

DatoCMS internal domain for the administrative area. Returns `null` if the credentials you are using cannot read the configuration of the project.

**`last_data_change_at`**

- Type: null, date-time
- Example: `"2017-03-30T09:29:14.872Z"`

Specifies the last time when a change of data occurred

**`locales`**

- Type: Array\<string\>
- Example: `["en"]`

Available locales

**`name`**

- Type: string
- Example: `"My Awesome Website"`

Site name

**`no_index`**

- Type: boolean

Whether the website needs to be indexed by search engines or not

**`require_2fa`**

- Type: boolean, null

Specifies whether all users of this site need to authenticate using two-factor authentication. Returns `null` if the credentials you are using cannot read the configuration of the project.

**`theme`**

- Type: object

Specifies the theme to use in administrative area. For monochromatic themes the response only includes type, hue, and logo — frontends must derive the palette from the hue. For custom themes the primary_color, light_color, accent_color, and dark_color fields are returned.

<details>
<summary>Show object format</summary>

**`type`**

- Type: enum
- Example: `"monochromatic"`

If type is monochromatic, the hue will determine the color palette. Dark color is a legacy property, and it won't be used on the interface

<details>
<summary>Show enum values</summary>

**`custom`**

Use custom color palette (deprecated)

**`monochromatic`**

Use monochromatic, accessible color palette

</details>

**`hue`**

- Type: integer, null
- Example: `16`

If the type is monochromatic, the value will fall between 0 and 359. If it's not, the value will be null.

**`logo`**

- Type: string, null
- Example: `"123"`

The upload ID that is used as the logo for the project

**`color_channel`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

<details>
<summary>Show deprecated</summary>

**`color`**

- Deprecated
- Type: object

An RGBA color value

Custom color palettes are deprecated.

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`green`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`blue`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`alpha`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

</details>

**`primary_color`**

- Deprecated
- Type: object

An RGBA color value

Custom color palettes are deprecated.

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`green`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`blue`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`alpha`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

</details>

**`light_color`**

- Deprecated
- Type: object

An RGBA color value

Custom color palettes are deprecated.

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`green`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`blue`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`alpha`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

</details>

**`accent_color`**

- Deprecated
- Type: object

An RGBA color value

Custom color palettes are deprecated.

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`green`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`blue`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`alpha`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

</details>

**`dark_color`**

- Deprecated
- Type: object

An RGBA color value

Custom color palettes are deprecated.

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`green`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`blue`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

**`alpha`**

- Type: integer
- Example: `128`

An integer color channel value (0-255)

</details>

</details>

</details>

**`timezone`**

- Type: string
- Example: `"Europe/London"`

Site default timezone

**`ip_tracking_enabled`**

- Type: boolean, null

Specifies whether you want IPs to be tracked in the Project usages section. Returns `null` if the credentials you are using cannot read the configuration of the project.

**`force_use_of_sandbox_environments`**

- Type: boolean

If enabled, blocks schema changes of primary environment

**`assets_cdn_default_settings`**

- Type: object

Allows setting default parameters for assets served through the CDN

<details>
<summary>Show object format</summary>

**`image`**

- Type: object

Allows setting default parameters for optimizing images served by the CDN

<details>
<summary>Show object format</summary>

**`q`**

- Type: integer
- Example: `50`

Controls the output quality of lossy file formats (jpg, pjpg, webp, avif, or jxr). Valid values are in the range 0 – 100 and the default is 75.

**`auto`**

- Type: Array\<string\>
- Example: `["compress", "format"]`

The auto parameter helps automating a baseline level of optimization. Specify one or more settings

**`cs`**

- Type: enum
- Example: `"srgb"`

Specifies the color space of the output image

<details>
<summary>Show enum values</summary>

**`srgb`**

Uses the sRGB colorspace which is an internet standard. This is the default

**`adobergb1998`**

Refers to the Adobe RGB (1998) color space, which provides accurate color reproduction from screen to print

**`tinysrgb`**

Reduces the color space metadata but may cause a slight shift in color values

**`strip`**

Removes the colorspace for maximum size reduction. Note that colors will still be rendered, but a colorspace will not be specified

**`origin`**

Keeps the color space of the origin image. This is the default value unless auto=compress

</details>

</details>

**`video`**

- Type: object

Allows setting default parameters for optimizing videos served by the CDN

<details>
<summary>Show object format</summary>

**`disable_serving_raw_videos`**

- Type: boolean

When true, attempting to retrieve raw video files directly instead of their optimized counterparts will result in a HTTP 422 status code

</details>

</details>

**`meta.created_at`**

- Type: date-time

Date of project creation

**`meta.improved_timezone_management`**

- Type: boolean

Whether the [Improved API Timezone Management](https://www.datocms.com/product-updates/improved-timezone-management) opt-in product update is active or not

**`meta.improved_hex_management`**

- Type: boolean

Whether the [Improved API Hex Management](https://www.datocms.com/product-updates/improved-hex-management) opt-in product update is active or not

**`meta.improved_gql_multilocale_fields`**

- Type: boolean

Whether the [Improved GraphQL multi-locale fields](https://www.datocms.com/product-updates/improved-gql-multilocale-fields) opt-in product update is active or not

**`meta.improved_gql_visibility_control`**

- Type: boolean

Whether the [Improved GraphQL visibility control](https://www.datocms.com/product-updates/improved-gql-visibility-control) opt-in product update is active or not

**`meta.improved_boolean_fields`**

- Type: boolean

Whether the [Improved boolean fields](https://www.datocms.com/product-updates/improved-boolean-fields) opt-in product update is active or not

**`meta.draft_mode_default`**

- Type: boolean

The default value for the draft mode option in all the environment's models

**`meta.improved_validation_at_publishing`**

- Type: boolean

Whether the [Improved validation at publishing](https://www.datocms.com/product-updates/force-validations-on-records-when-publishing) opt-in product update is active or not

**`meta.improved_exposure_of_inline_blocks_in_cda`**

- Type: boolean

Whether the [Improved exposure of inline blocks in the Content Delivery API](https://www.datocms.com/product-updates/improved-exposure-of-inline-blocks-in-cda) opt-in product update is active or not

**`meta.improved_items_listing`**

- Type: boolean

Whether the [Improved items listing](https://www.datocms.com/product-updates/improved-items-listing) opt-in product update is active or not

**`meta.milliseconds_in_datetime`**

- Type: boolean

Whether the [Milliseconds in datetime](https://www.datocms.com/product-updates/milliseconds-in-datetime) opt-in product update is active or not

**`meta.non_localized_focal_points`**

- Type: boolean

Whether the [Non-localized focal points](https://www.datocms.com/product-updates/non-localized-focal-points) opt-in product update is active or not

**`meta.allow_custom_theme`**

- Type: boolean

Whether the project can still use a custom color palette for the theme. Custom palettes are deprecated.

**`meta.custom_upload_storage_settings`**

- Type: boolean

Whether the site has custom upload storage settings

**`item_types`**

- Type: Array<[ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)>

**`owner`**

- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

<details>
<summary>Show deprecated</summary>

**`account`**

- Deprecated
- Type: null, [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md)

Please user the owner relationship instead

</details>

---

# Content Management API — Retrieve the site

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site/self.md

## Query parameters

**`include`**

- Type: string
- Example: `"item_types,item_types.fields"`

Comma-separated list of [relationship paths](https://jsonapi.org/format/#fetching-includes). A relationship path is a dot-separated list of relationship names. Allowed relationship paths: `item_types`, `item_types.fields`, `item_types.fieldsets`, `item_types.singleton_item`, `account`, `owner`.

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`site`**

- Type: string
- Example: `"name,domain,google_maps_api_token,owner"`

Attributes and relationships to return for each `site` that this endpoint returns.

**`item_type`**

- Type: string
- Example: `"name,api_key,collection_appearance,singleton_item"`

Attributes and relationships to return for each `item_type` that this endpoint returns.

**`field`**

- Type: string
- Example: `"label,field_type,localized,item_type"`

Attributes and relationships to return for each `field` that this endpoint returns.

**`fieldset`**

- Type: string
- Example: `"title,hint,collapsible,item_type"`

Attributes and relationships to return for each `fieldset` that this endpoint returns.

**`item`**

- Type: string
- Example: `"title,content,category,item_type"`

Attributes and relationships to return for each `item` that this endpoint returns.

**`account`**

- Type: string
- Example: `"email,first_name,last_name"`

Attributes and relationships to return for each `account` that this endpoint returns.

**`organization`**

- Type: string
- Example: `"name"`

Attributes and relationships to return for each `organization` that this endpoint returns.

</details>

## Returns

Returns a resource object of type [site](/docs/content-management-api/resources/site.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const site = await client.site.find();

  // Check the 'Returned output' tab for the result ☝️
  console.log(site);
}

run();
```

Returned output

```javascript
{
  id: "155",
  domain: "admin.my-awesome-website.com",
  favicon: "123",
  global_seo: {},
  google_maps_api_token: "xxxxxxxxxxxxx",
  imgix_host: "www.datocms-assets.com",
  internal_domain: "my-website.admin.datocms.com",
  last_data_change_at: "2017-03-30T09:29:14.872Z",
  locales: ["en"],
  name: "My Awesome Website",
  no_index: true,
  require_2fa: false,
  theme: { type: "monochromatic", hue: 16, logo: "123" },
  timezone: "Europe/London",
  ip_tracking_enabled: true,
  force_use_of_sandbox_environments: true,
  assets_cdn_default_settings: { image: {}, video: {} },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    improved_timezone_management: true,
    improved_hex_management: true,
    improved_gql_multilocale_fields: true,
    improved_gql_visibility_control: true,
    improved_boolean_fields: true,
    draft_mode_default: true,
    improved_validation_at_publishing: true,
    improved_exposure_of_inline_blocks_in_cda: true,
    improved_items_listing: true,
    milliseconds_in_datetime: true,
    non_localized_focal_points: true,
    allow_custom_theme: true,
  },
  item_types: [{ type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" }],
  owner: { type: "account", id: "312" },
}
```

---

# Content Management API — Update the site's settings

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site/update.md

## Body parameters

**`no_index`**

- Optional
- Type: boolean

Whether the website needs to be indexed by search engines or not

**`favicon`**

- Optional
- Type: string, null
- Example: `"123"`

The upload id for the favicon

**`global_seo`**

- Optional
- Type: object, null

Specifies default global settings

<details>
<summary>Show object format</summary>

**`site_name`**

- Optional
- Type: string
- Example: `"My Awesome Website"`

Site name, used in social sharing

**`fallback_seo`**

- Optional
- Type: object

<details>
<summary>Show object format</summary>

**`title`**

- Required
- Type: string
- Example: `"Default meta title"`

**`description`**

- Required
- Type: string
- Example: `"Default meta description"`

**`image`**

- Required
- Type: null, string
- Example: `"123"`

The id of the image

**`twitter_card`**

- Optional
- Type: null, enum
- Example: `"summary_large_image"`

Determines how a Twitter link preview is shown

<details>
<summary>Show enum values</summary>

**`summary`**

- Optional

Twitter summary card

**`summary_large_image`**

- Optional

Twitter summary card with large image

</details>

</details>

**`title_suffix`**

- Optional
- Type: null, string
- Example: `" - My Awesome Website"`

Title meta tag suffix

**`facebook_page_url`**

- Optional
- Type: null, string
- Example: `"http://facebook.com/awesomewebsite"`

URL of facebook page

**`twitter_account`**

- Optional
- Type: null, string
- Example: `"@awesomewebsite"`

Twitter account associated to website

</details>

**`name`**

- Optional
- Type: string
- Example: `"My Awesome Website"`

Site name

**`theme`**

- Optional
- Type: object

**`locales`**

- Optional
- Type: Array\<string\>
- Example: `["en"]`

Available locales

**`timezone`**

- Optional
- Type: string
- Example: `"Europe/London"`

Site default timezone

**`require_2fa`**

- Optional
- Type: boolean

Specifies whether all users of this site need to authenticate using two-factor authentication.

**`ip_tracking_enabled`**

- Optional
- Type: boolean

Specifies whether you want IPs to be tracked in the Project usages section.

**`force_use_of_sandbox_environments`**

- Optional
- Type: boolean

If enabled, blocks schema changes of primary environment

**`meta.improved_timezone_management`**

- Optional
- Type: boolean

Whether the [Improved API Timezone Management](https://www.datocms.com/product-updates/improved-timezone-management) opt-in product update is active or not

**`meta.improved_hex_management`**

- Optional
- Type: boolean

Whether the [Improved API Hex Management](https://www.datocms.com/product-updates/improved-hex-management) opt-in product update is active or not

**`meta.improved_gql_multilocale_fields`**

- Optional
- Type: boolean

Whether the [Improved GraphQL multi-locale fields](https://www.datocms.com/product-updates/improved-gql-multilocale-fields) opt-in product update is active or not

**`meta.improved_gql_visibility_control`**

- Optional
- Type: boolean

Whether the [Improved GraphQL visibility control](https://www.datocms.com/product-updates/improved-gql-visibility-control) opt-in product update is active or not

**`meta.improved_boolean_fields`**

- Optional
- Type: boolean

Whether the [Improved boolean fields](https://www.datocms.com/product-updates/improved-boolean-fields) opt-in product update is active or not

**`meta.draft_mode_default`**

- Optional
- Type: boolean

The default value for the draft mode option in all the environment's models

**`meta.improved_validation_at_publishing`**

- Optional
- Type: boolean

Whether the [Improved validation at publishing](https://www.datocms.com/product-updates/force-validations-on-records-when-publishing) opt-in product update is active or not

**`meta.custom_upload_storage_settings`**

- Optional
- Type: boolean

Whether the site has custom upload storage settings

**`meta.improved_exposure_of_inline_blocks_in_cda`**

- Optional
- Type: boolean

Whether the [Improved exposure of inline blocks in the Content Delivery API](https://www.datocms.com/product-updates/improved-exposure-of-inline-blocks-in-cda) opt-in product update is active or not

**`meta.improved_items_listing`**

- Optional
- Type: boolean

Whether the [Improved items listing](https://www.datocms.com/product-updates/improved-items-listing) opt-in product update is active or not

**`meta.milliseconds_in_datetime`**

- Optional
- Type: boolean

Whether the [Milliseconds in datetime](https://www.datocms.com/product-updates/milliseconds-in-datetime) opt-in product update is active or not

**`meta.non_localized_focal_points`**

- Optional
- Type: boolean

Whether the [Non-localized focal points](https://www.datocms.com/product-updates/non-localized-focal-points) opt-in product update is active or not

## Returns

Returns a resource object of type [site](/docs/content-management-api/resources/site.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const site = await client.site.update({});

  // Check the 'Returned output' tab for the result ☝️
  console.log(site);
}

run();
```

Returned output

```javascript
{
  id: "155",
  domain: "admin.my-awesome-website.com",
  favicon: "123",
  global_seo: {},
  google_maps_api_token: "xxxxxxxxxxxxx",
  imgix_host: "www.datocms-assets.com",
  internal_domain: "my-website.admin.datocms.com",
  last_data_change_at: "2017-03-30T09:29:14.872Z",
  locales: ["en"],
  name: "My Awesome Website",
  no_index: true,
  require_2fa: false,
  theme: { type: "monochromatic", hue: 16, logo: "123" },
  timezone: "Europe/London",
  ip_tracking_enabled: true,
  force_use_of_sandbox_environments: true,
  assets_cdn_default_settings: { image: {}, video: {} },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    improved_timezone_management: true,
    improved_hex_management: true,
    improved_gql_multilocale_fields: true,
    improved_gql_visibility_control: true,
    improved_boolean_fields: true,
    draft_mode_default: true,
    improved_validation_at_publishing: true,
    improved_exposure_of_inline_blocks_in_cda: true,
    improved_items_listing: true,
    milliseconds_in_datetime: true,
    non_localized_focal_points: true,
    allow_custom_theme: true,
  },
  item_types: [{ type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" }],
  owner: { type: "account", id: "312" },
}
```

---

# Content Management API — Model/Block model

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type.md

The way you define the kind of content you can edit inside a DatoCMS project passes through the concept of **models** and **block models**. For backward-compatibility reasons, the API refers to both as "item types".

## Models

Models are much like database tables — they define the structure of your main content types (e.g., blog posts, products, landing pages). Each model is composed of fields with custom validations. Records created from models exist independently and can be referenced by other records through link fields.

## Block Models

Block models define complex and repeatable structures that can be embedded inside records. They are the foundation behind features like [Modular Content](/docs/content-modelling/modular-content.md) and [Structured Text](/docs/content-modelling/structured-text.md).

### Key differences:

-   **Models** create standalone records that can be referenced and have independent value
-   **Block models** create blocks that only exist within parent records and cannot be referenced via link fields
-   Block models defined in the library can be reused across different models
-   When a record gets deleted, all the blocks it contains are deleted with it
-   Blocks do not count towards your plan's records limit

You can distinguish between models and block models using the `modular_block` attribute: `true` indicates a block model, `false` indicates a regular model.

## Object payload

**`id`**

- Type: string
- Example: `"DxMaW10UQiCmZcuuA-IkkA"`

RFC 4122 UUID of item type expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"item_type"`.

**`name`**

- Type: string
- Example: `"Blog post"`

Name of the model/block model

**`api_key`**

- Type: string
- Example: `"post"`

API key of the model/block model

**`singleton`**

- Type: boolean

Whether the model is single-instance or not. This property only applies to models, not block models

**`sortable`**

- Type: boolean

Whether editors can sort records via drag & drop or not. Must be false for block models

**`modular_block`**

- Type: boolean

Whether this is a block model or not. Block models define structures that can be embedded inside records, while regular models create standalone records

**`tree`**

- Type: boolean

Whether editors can organize records in a tree or not. Must be false for block models

**`ordering_direction`**

- Type: enum, null

If an ordering field is set, this field specifies the sorting direction. This property does not apply to block models

<details>
<summary>Show enum values</summary>

**`asc`**

Ascending order

**`desc`**

Descending order

</details>

**`ordering_meta`**

- Type: enum, null
- Example: `"created_at"`

Specifies the model's sorting method. Cannot be set in concurrency with ordering_field. This property does not apply to block models

<details>
<summary>Show enum values</summary>

**`created_at`**

Order by date of creation

**`updated_at`**

Order by date of last update

**`first_published_at`**

Order by date of first publication

**`published_at`**

Order by date of last publication

</details>

**`draft_mode_active`**

- Type: boolean

Whether draft/published mode is active or not. Must be false for block models

**`all_locales_required`**

- Type: boolean

Whether we require all the project locales to be present for each localized field or not

**`collection_appearance`**

- Type: enum
- Example: `"compact"`

The way the model/block model collection should be presented to the editors

<details>
<summary>Show enum values</summary>

**`compact`**

Compact view

**`table`**

Tabular view

</details>

**`hint`**

- Type: string, null
- Example: `"Blog posts will be shown in our website under the Blog section"`

A hint shown to editors to help them understand the purpose of this model/block model

**`inverse_relationships_enabled`**

- Type: boolean

Whether inverse relationships fields are expressed in GraphQL or not. Must be false for block models

**`draft_saving_active`**

- Type: boolean

Whether draft records can be saved without satisfying the validations or not. Must be false for block models

**`meta.has_singleton_item`**

- Type: boolean

If this model is single-instance, this tells whether the single-instance record has already been created or not. This property only applies to models, not block models

**`singleton_item`**

- Type: [ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md), null

The record instance related to this model. This relationship only applies to single-instance models, not block models

**`fields`**

- Type: Array<[ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md)>

The list of fields for this model/block model

**`fieldsets`**

- Type: Array<[ResourceLinkage\<"fieldset"\>](https://www-draft.datocms.com/docs/content-management-api/resources/fieldset.md)>

The list of fieldsets for this model/block model

**`presentation_title_field`**

- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as presentation title

**`presentation_image_field`**

- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as presentation image

**`title_field`**

- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback title for SEO purposes. This relationship does not apply to block models

**`image_preview_field`**

- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback image for SEO purposes. This relationship does not apply to block models

**`excerpt_field`**

- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback description for SEO purposes. This relationship does not apply to block models

**`ordering_field`**

- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field upon which the collection is sorted. This relationship does not apply to block models

**`workflow`**

- Type: [ResourceLinkage\<"workflow"\>](https://www-draft.datocms.com/docs/content-management-api/resources/workflow.md), null

The workflow to enforce on records

<details>
<summary>Show deprecated</summary>

**`collection_appeareance`**

- Deprecated
- Type: enum
- Example: `"compact"`

The way the model collection should be presented to the editors

This field contains a typo and will be removed in future versions: use `collection_appearance` instead

<details>
<summary>Show enum values</summary>

**`compact`**

Compact view

**`table`**

Tabular view

</details>

**`has_singleton_item`**

- Deprecated
- Type: boolean

If this model is single-instance, this tells whether the single-instance record has already been created or not. This property only applies to models, not block models

This field will be removed in future versions: instead, use the equivalent `has_singleton_item` field in the meta collection

</details>

---

# Content Management API — Create a new model/block model

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type/create.md

## Query parameters

**`skip_menu_item_creation`**

- Type: boolean

Skip the creation of a menu item linked to the model

**`menu_item_id`**

- Type: string
- Example: `"FF-P5of6Qp-DD2w0xoaa6Q"`

Explicitely specify the ID of the menu item that will be linked to the model

**`schema_menu_item_id`**

- Type: string
- Example: `"FF-P5of6Qp-DD2w0xoaa6Q"`

Explicitely specify the ID of the schema menu item that will be linked to the model

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"DxMaW10UQiCmZcuuA-IkkA"`

RFC 4122 UUID of item type expressed in URL-safe base64 format

**`name`**

- Required
- Type: string
- Example: `"Blog post"`

Name of the model/block model

**`api_key`**

- Required
- Type: string
- Example: `"post"`

API key of the model/block model

**`singleton`**

- Optional
- Type: boolean

Whether the model is single-instance or not. This property only applies to models, not block models

**`all_locales_required`**

- Optional
- Type: boolean

Whether we require all the project locales to be present for each localized field or not

**`sortable`**

- Optional
- Type: boolean

Whether editors can sort records via drag & drop or not. Must be false for block models

**`modular_block`**

- Optional
- Type: boolean

Whether this is a block model or not. Block models define structures that can be embedded inside records, while regular models create standalone records

**`draft_mode_active`**

- Optional
- Type: boolean

Whether draft/published mode is active or not. Must be false for block models

**`draft_saving_active`**

- Optional
- Type: boolean

Whether draft records can be saved without satisfying the validations or not. Must be false for block models

**`tree`**

- Optional
- Type: boolean

Whether editors can organize records in a tree or not. Must be false for block models

**`ordering_direction`**

- Optional
- Type: enum, null

If an ordering field is set, this field specifies the sorting direction. This property does not apply to block models

<details>
<summary>Show enum values</summary>

**`asc`**

- Optional

Ascending order

**`desc`**

- Optional

Descending order

</details>

**`ordering_meta`**

- Optional
- Type: enum, null
- Example: `"created_at"`

Specifies the model's sorting method. Cannot be set in concurrency with ordering_field. This property does not apply to block models

<details>
<summary>Show enum values</summary>

**`created_at`**

- Optional

Order by date of creation

**`updated_at`**

- Optional

Order by date of last update

**`first_published_at`**

- Optional

Order by date of first publication

**`published_at`**

- Optional

Order by date of last publication

</details>

**`collection_appearance`**

- Optional
- Type: enum
- Example: `"compact"`

The way the model/block model collection should be presented to the editors

<details>
<summary>Show enum values</summary>

**`compact`**

- Optional

Compact view

**`table`**

- Optional

Tabular view

</details>

**`hint`**

- Optional
- Type: string, null
- Example: `"Blog posts will be shown in our website under the Blog section"`

A hint shown to editors to help them understand the purpose of this model/block model

**`inverse_relationships_enabled`**

- Optional
- Type: boolean

Whether inverse relationships fields are expressed in GraphQL or not. Must be false for block models

**`ordering_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field upon which the collection is sorted. This relationship does not apply to block models

**`presentation_title_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as presentation title

**`presentation_image_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as presentation image

**`title_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback title for SEO purposes. This relationship does not apply to block models

**`image_preview_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback image for SEO purposes. This relationship does not apply to block models

**`excerpt_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback description for SEO purposes. This relationship does not apply to block models

**`workflow`**

- Optional
- Type: [ResourceLinkage\<"workflow"\>](https://www-draft.datocms.com/docs/content-management-api/resources/workflow.md), null

The workflow to enforce on records

<details>
<summary>Show deprecated</summary>

**`collection_appeareance`**

- Deprecated
- Type: enum
- Example: `"compact"`

The way the model collection should be presented to the editors

This field contains a typo and will be removed in future versions: use `collection_appearance` instead

<details>
<summary>Show enum values</summary>

**`compact`**

- Optional

Compact view

**`table`**

- Optional

Tabular view

</details>

</details>

## Returns

Returns a resource object of type [item\_type](/docs/content-management-api/resources/item-type.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemType = await client.itemTypes.create({
    name: "Blog post",
    api_key: "post",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemType);
}

run();
```

Returned output

```javascript
{
  id: "DxMaW10UQiCmZcuuA-IkkA",
  name: "Blog post",
  api_key: "post",
  singleton: false,
  sortable: true,
  modular_block: false,
  tree: false,
  ordering_direction: null,
  ordering_meta: "created_at",
  draft_mode_active: false,
  all_locales_required: false,
  collection_appearance: "compact",
  hint: "Blog posts will be shown in our website under the Blog section",
  inverse_relationships_enabled: false,
  draft_saving_active: false,
  meta: { has_singleton_item: false },
  singleton_item: null,
  fields: [{ type: "field", id: "Pkg-oztERp6o-Rj76nYKJg" }],
  fieldsets: [{ type: "fieldset", id: "93Y1C2sySkG4Eg0atBRIwg" }],
  presentation_title_field: null,
  presentation_image_field: null,
  title_field: null,
  image_preview_field: null,
  excerpt_field: null,
  ordering_field: null,
  workflow: null,
}
```

---

# Content Management API — Update a model/block model

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type/update.md

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"Blog post"`

Name of the model/block model

**`api_key`**

- Optional
- Type: string
- Example: `"post"`

API key of the model/block model

**`collection_appearance`**

- Optional
- Type: enum
- Example: `"compact"`

The way the model/block model collection should be presented to the editors

<details>
<summary>Show enum values</summary>

**`compact`**

- Optional

Compact view

**`table`**

- Optional

Tabular view

</details>

**`singleton`**

- Optional
- Type: boolean

Whether the model is single-instance or not. This property only applies to models, not block models

**`all_locales_required`**

- Optional
- Type: boolean

Whether we require all the project locales to be present for each localized field or not

**`sortable`**

- Optional
- Type: boolean

Whether editors can sort records via drag & drop or not. Must be false for block models

**`modular_block`**

- Optional
- Type: boolean

Whether this is a block model or not. Block models define structures that can be embedded inside records, while regular models create standalone records

**`draft_mode_active`**

- Optional
- Type: boolean

Whether draft/published mode is active or not. Must be false for block models

**`draft_saving_active`**

- Optional
- Type: boolean

Whether draft records can be saved without satisfying the validations or not. Must be false for block models

**`tree`**

- Optional
- Type: boolean

Whether editors can organize records in a tree or not. Must be false for block models

**`ordering_direction`**

- Optional
- Type: enum, null

If an ordering field is set, this field specifies the sorting direction. This property does not apply to block models

<details>
<summary>Show enum values</summary>

**`asc`**

- Optional

Ascending order

**`desc`**

- Optional

Descending order

</details>

**`ordering_meta`**

- Optional
- Type: enum, null
- Example: `"created_at"`

Specifies the model's sorting method. Cannot be set in concurrency with ordering_field. This property does not apply to block models

<details>
<summary>Show enum values</summary>

**`created_at`**

- Optional

Order by date of creation

**`updated_at`**

- Optional

Order by date of last update

**`first_published_at`**

- Optional

Order by date of first publication

**`published_at`**

- Optional

Order by date of last publication

</details>

**`hint`**

- Optional
- Type: string, null
- Example: `"Blog posts will be shown in our website under the Blog section"`

A hint shown to editors to help them understand the purpose of this model/block model

**`inverse_relationships_enabled`**

- Optional
- Type: boolean

Whether inverse relationships fields are expressed in GraphQL or not. Must be false for block models

**`meta.has_singleton_item`**

- Optional
- Type: boolean

If this model is single-instance, this tells whether the single-instance record has already been created or not. This property only applies to models, not block models

**`ordering_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field upon which the collection is sorted. This relationship does not apply to block models

**`presentation_title_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as presentation title

**`presentation_image_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as presentation image

**`title_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback title for SEO purposes. This relationship does not apply to block models

**`image_preview_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback image for SEO purposes. This relationship does not apply to block models

**`excerpt_field`**

- Optional
- Type: [ResourceLinkage\<"field"\>](https://www-draft.datocms.com/docs/content-management-api/resources/field.md), null

The field to use as fallback description for SEO purposes. This relationship does not apply to block models

**`workflow`**

- Optional
- Type: [ResourceLinkage\<"workflow"\>](https://www-draft.datocms.com/docs/content-management-api/resources/workflow.md), null

The workflow to enforce on records

<details>
<summary>Show deprecated</summary>

**`collection_appeareance`**

- Deprecated
- Type: enum
- Example: `"compact"`

The way the model collection should be presented to the editors

This field contains a typo and will be removed in future versions: use `collection_appearance` instead

<details>
<summary>Show enum values</summary>

**`compact`**

- Optional

Compact view

**`table`**

- Optional

Tabular view

</details>

</details>

## Returns

Returns a resource object of type [item\_type](/docs/content-management-api/resources/item-type.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const itemType = await client.itemTypes.update(modelIdOrApiKey, {
    id: "DxMaW10UQiCmZcuuA-IkkA",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemType);
}

run();
```

Returned output

```javascript
{
  id: "DxMaW10UQiCmZcuuA-IkkA",
  name: "Blog post",
  api_key: "post",
  singleton: false,
  sortable: true,
  modular_block: false,
  tree: false,
  ordering_direction: null,
  ordering_meta: "created_at",
  draft_mode_active: false,
  all_locales_required: false,
  collection_appearance: "compact",
  hint: "Blog posts will be shown in our website under the Blog section",
  inverse_relationships_enabled: false,
  draft_saving_active: false,
  meta: { has_singleton_item: false },
  singleton_item: null,
  fields: [{ type: "field", id: "Pkg-oztERp6o-Rj76nYKJg" }],
  fieldsets: [{ type: "fieldset", id: "93Y1C2sySkG4Eg0atBRIwg" }],
  presentation_title_field: null,
  presentation_image_field: null,
  title_field: null,
  image_preview_field: null,
  excerpt_field: null,
  ordering_field: null,
  workflow: null,
}
```

---

# Content Management API — List all models/block models

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type/instances.md

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`item_type`**

- Type: string
- Example: `"name,api_key,collection_appearance,singleton_item"`

Attributes and relationships to return for each `item_type` that this endpoint returns.

</details>

## Returns

Returns an array of resource objects of type [item\_type](/docs/content-management-api/resources/item-type.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemTypes = await client.itemTypes.list();

  for (const itemType of itemTypes) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(itemType);
  }
}

run();
```

Returned output

```javascript
{
  id: "DxMaW10UQiCmZcuuA-IkkA",
  name: "Blog post",
  api_key: "post",
  singleton: false,
  sortable: true,
  modular_block: false,
  tree: false,
  ordering_direction: null,
  ordering_meta: "created_at",
  draft_mode_active: false,
  all_locales_required: false,
  collection_appearance: "compact",
  hint: "Blog posts will be shown in our website under the Blog section",
  inverse_relationships_enabled: false,
  draft_saving_active: false,
  meta: { has_singleton_item: false },
  singleton_item: null,
  fields: [{ type: "field", id: "Pkg-oztERp6o-Rj76nYKJg" }],
  fieldsets: [{ type: "fieldset", id: "93Y1C2sySkG4Eg0atBRIwg" }],
  presentation_title_field: null,
  presentation_image_field: null,
  title_field: null,
  image_preview_field: null,
  excerpt_field: null,
  ordering_field: null,
  workflow: null,
}
```

---

# Content Management API — Retrieve a model/block model

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type/self.md

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`item_type`**

- Type: string
- Example: `"name,api_key,collection_appearance,singleton_item"`

Attributes and relationships to return for each `item_type` that this endpoint returns.

**`field`**

- Type: string
- Example: `"label,field_type,localized,item_type"`

Attributes and relationships to return for each `field` that this endpoint returns.

</details>

## Returns

Returns a resource object of type [item\_type](/docs/content-management-api/resources/item-type.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const itemType = await client.itemTypes.find(modelIdOrApiKey);

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemType);
}

run();
```

Returned output

```javascript
{
  id: "DxMaW10UQiCmZcuuA-IkkA",
  name: "Blog post",
  api_key: "post",
  singleton: false,
  sortable: true,
  modular_block: false,
  tree: false,
  ordering_direction: null,
  ordering_meta: "created_at",
  draft_mode_active: false,
  all_locales_required: false,
  collection_appearance: "compact",
  hint: "Blog posts will be shown in our website under the Blog section",
  inverse_relationships_enabled: false,
  draft_saving_active: false,
  meta: { has_singleton_item: false },
  singleton_item: null,
  fields: [{ type: "field", id: "Pkg-oztERp6o-Rj76nYKJg" }],
  fieldsets: [{ type: "fieldset", id: "93Y1C2sySkG4Eg0atBRIwg" }],
  presentation_title_field: null,
  presentation_image_field: null,
  title_field: null,
  image_preview_field: null,
  excerpt_field: null,
  ordering_field: null,
  workflow: null,
}
```

---

# Content Management API — Duplicate model/block model

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type/duplicate.md

## Returns

Returns a resource object of type [item\_type](/docs/content-management-api/resources/item-type.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const itemType = await client.itemTypes.duplicate(modelIdOrApiKey);

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemType);
}

run();
```

Returned output

```javascript
{
  id: "DxMaW10UQiCmZcuuA-IkkA",
  name: "Blog post",
  api_key: "post",
  singleton: false,
  sortable: true,
  modular_block: false,
  tree: false,
  ordering_direction: null,
  ordering_meta: "created_at",
  draft_mode_active: false,
  all_locales_required: false,
  collection_appearance: "compact",
  hint: "Blog posts will be shown in our website under the Blog section",
  inverse_relationships_enabled: false,
  draft_saving_active: false,
  meta: { has_singleton_item: false },
  singleton_item: null,
  fields: [{ type: "field", id: "Pkg-oztERp6o-Rj76nYKJg" }],
  fieldsets: [{ type: "fieldset", id: "93Y1C2sySkG4Eg0atBRIwg" }],
  presentation_title_field: null,
  presentation_image_field: null,
  title_field: null,
  image_preview_field: null,
  excerpt_field: null,
  ordering_field: null,
  workflow: null,
}
```

---

# Content Management API — Delete a model/block model

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type/destroy.md

## Query parameters

**`skip_menu_items_deletion`**

- Type: boolean

Skip the deletion of the menu items linked to the model

## Returns

Returns a resource object of type [item\_type](/docs/content-management-api/resources/item-type.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const itemType = await client.itemTypes.destroy(modelIdOrApiKey);

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemType);
}

run();
```

Returned output

```javascript
{
  id: "DxMaW10UQiCmZcuuA-IkkA",
  name: "Blog post",
  api_key: "post",
  singleton: false,
  sortable: true,
  modular_block: false,
  tree: false,
  ordering_direction: null,
  ordering_meta: "created_at",
  draft_mode_active: false,
  all_locales_required: false,
  collection_appearance: "compact",
  hint: "Blog posts will be shown in our website under the Blog section",
  inverse_relationships_enabled: false,
  draft_saving_active: false,
  meta: { has_singleton_item: false },
  singleton_item: null,
  fields: [{ type: "field", id: "Pkg-oztERp6o-Rj76nYKJg" }],
  fieldsets: [{ type: "fieldset", id: "93Y1C2sySkG4Eg0atBRIwg" }],
  presentation_title_field: null,
  presentation_image_field: null,
  title_field: null,
  image_preview_field: null,
  excerpt_field: null,
  ordering_field: null,
  workflow: null,
}
```

---

# Content Management API — List models referencing another model/block

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type/referencing.md

Returns all models that reference the specified item type, either directly through fields or indirectly through nested blocks.

## For Block Models (modular\_block: true)

Returns all models that can embed the specified block model, including:

**Direct embedding via:**

-   `rich_text` fields (Modular Content) — specified in the `rich_text_blocks` validator
-   `single_block` fields — specified in the `single_block_blocks` validator
-   `structured_text` fields — specified in the `structured_text_blocks` or `structured_text_inline_blocks` validators

**Indirect embedding:**

-   Models that embed other block models, which themselves contain fields that can embed the target block (recursive traversal through nested block structures)

## For Regular Models (modular\_block: false)

Returns all models that reference the specified model, including:

**Direct references via:**

-   `link` fields (Single link) — specified in the `item_item_type` validator
-   `links` fields (Multiple links) — specified in the `items_item_type` validator
-   `structured_text` fields — specified in the `structured_text_links` validator

**Indirect references:**

-   Models that embed block models containing fields that reference the target model (traversal through nested block structures to find references within blocks)

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`item_type`**

- Type: string
- Example: `"name,api_key,collection_appearance,singleton_item"`

Attributes and relationships to return for each `item_type` that this endpoint returns.

</details>

## Returns

Returns an array of resource objects of type [item\_type](/docs/content-management-api/resources/item-type.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const itemTypes = await client.itemTypes.referencing(modelIdOrApiKey);

  for (const itemType of itemTypes) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(itemType);
  }
}

run();
```

Returned output

```javascript
{
  id: "DxMaW10UQiCmZcuuA-IkkA",
  name: "Blog post",
  api_key: "post",
  singleton: false,
  sortable: true,
  modular_block: false,
  tree: false,
  ordering_direction: null,
  ordering_meta: "created_at",
  draft_mode_active: false,
  all_locales_required: false,
  collection_appearance: "compact",
  hint: "Blog posts will be shown in our website under the Blog section",
  inverse_relationships_enabled: false,
  draft_saving_active: false,
  meta: { has_singleton_item: false },
  singleton_item: null,
  fields: [{ type: "field", id: "Pkg-oztERp6o-Rj76nYKJg" }],
  fieldsets: [{ type: "fieldset", id: "93Y1C2sySkG4Eg0atBRIwg" }],
  presentation_title_field: null,
  presentation_image_field: null,
  title_field: null,
  image_preview_field: null,
  excerpt_field: null,
  ordering_field: null,
  workflow: null,
}
```

---

# Content Management API — Field

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field.md

DatoCMS offers a number of different fields that you can combine together to create a [Model](/docs/content-management-api/resources/item-type.md). Using the database metaphore, fields are like table columns, and when creating them you need to specify their type (`string`, `float`, etc.) and any required validation.

### Different field types require different settings

When looking at a field resource, you have to pay attention to two particular properties, `validators` and `appearance`.

The `validators` property expresses the set of validations to be performed server-side on a specific field value for it to be considered valid, while the `appearance` property lets you specify *how* the field itself will be presented inside the form to the final editor.

For both properties, the value to specify depends on the type of field itself. For example, you can add a "Limit character count" validation to a *Single-line string* field, or set its appearence to "Show it as heading", but they won't be accepted for a ie. *Color* field, as it supports different validations and appearance settings.

### Specifying validations

The `validators` property requires an object whose keys are the validations that you want to be enforced, and the values are objects representing any settings that the validation itself requires. If the validation doesn't have additional settings, you just pass down an empty object.

This is a valid example for a *Single-line string* field:

```js
{
  "validators": {
    // "required" validator has no settings
    "required": {},
    // "length" validator requires "min" and/or "max" properties
    "length": { "min": 80 }
  }
}
```

Below you'll find a summary of all the validators available for each field type with their settings.

Some validators are required for a specific type of field. For example, the *Modular Content* field needs to have a `rich_text_blocks` validator, specifying which types of blocks it can contain.

### Specifying the appearance

The `appearance` property requires an object with three specific properties: `editor`, `parameters` and `addons`.

The `editor` represents the type of editor that the users will see inside the form to change the value of this specific field. Depending on the type of field, DatoCMS offers a number of different editors for you to choose from. The `parameters` property is an object representing any additional settings that the editor itself might require.

This is a valid example for a *Single-line string* field:

```js
{
  "appearance": {
    // single_line is a DatoCMS built-in editor that you can use with single-line string fields
    "editor": "single_line",
    // each built-in editor has specific settings
    "parameters": { "heading": true, "placeholder": "My blog post title" },
    "addons": []
  },
}
```

Following you'll find a summary of all the editors available for each field type with their settings.

#### Setting the appearance to a field editor provided by a plugin

If the project contains a plugin that exposes [manual field editors](/docs/plugin-sdk/manual-field-extensions.md), you can also configure the field to be presented with it instead of using one of the built-in editors.

In this case:

-   the `editor` property is the plugin's project-specific autogenerated UUID. You can get it from the last part of the plugin's URL within your project's Configuration screen (e.g. `https://your-project.admin.datocms.com/configuration/plugins/PLUGIN_UUID/`), or via API with a [List all plugins](/docs/content-management-api/resources/plugin/instances.md) call.
-   the `field_extension` property must be the ID of the specific manual field editor that the plugin exposes. This is set in the plugin's own source code, within a `manualFieldExtension()` call in its entry point (usually something like `index.tsx`).
-   the `parameters` property must provide a configuration object compatible with the [config screen of the manual field extension](/docs/plugin-sdk/manual-field-extensions.md#add-per-field-config-screens-to-manual-field-extensions), or an empty object if it doesn't require any configuration.

```js
{
  "appearance": {
    // "2132" is a the ID of a plugin exposing a manual field editor
    "editor": "2134",
    // "starRating" is a manual field editor exposed by the plugin
    "field_extension": "starRating",
    // this is a valid configuration for the "starRating" field editor
    "parameters": { "maxRating": 5, "starsColor": "#ff0000" },
    "addons": []
  },
}
```

#### Configuring manual field addons

If the project contains plugins that expose [manual field addons](/docs/plugin-sdk/manual-field-extensions.md), you can also add them to the field via the `addons` property.

```js
{
  "appearance": {
    "editor": "single_line",
    "parameters": { "heading": true, "placeholder": "My blog post title" },
    "addons": [
      {
        // "2138" is a the ID of a plugin exposing a manual addon editor
        "id": "2138",
        // "loremIpsumGenerator" is a manual field addon exposed by the plugin
        "field_extension": "loremIpsumGenerator",
        // this is a valid configuration for the "loremIpsumGenerator" field addon
        "parameters": { "sentences": 2 },
      }
    ]
  },
}
```

### Available field types

<details>
<summary>Single-line string (string)</summary>

| Property | Value |
| --- | --- |
| Code | `string` |
| Built-in editors for the field | `single_line`, `string_radio_group`, `string_select` |
| Available validators | `required`, `unique`, `length`, `format`, `enum` |

</details>

<details>
<summary>Multi-line text (text)</summary>

| Property | Value |
| --- | --- |
| Code | `text` |
| Built-in editors for the field | `markdown`, `wysiwyg`, `textarea` |
| Available validators | `required`, `length`, `format`, `sanitized_html` |

</details>

<details>
<summary>Boolean (boolean)</summary>

| Property | Value |
| --- | --- |
| Code | `boolean` |
| Built-in editors for the field | `boolean`, `boolean_radio_group` |
| Available validators | no validators available |

</details>

<details>
<summary>Integer (integer)</summary>

| Property | Value |
| --- | --- |
| Code | `integer` |
| Built-in editors for the field | `integer` |
| Available validators | `required`, `number_range` |

</details>

<details>
<summary>Float (float)</summary>

| Property | Value |
| --- | --- |
| Code | `float` |
| Built-in editors for the field | `float` |
| Available validators | `required`, `number_range` |

</details>

<details>
<summary>Date (date)</summary>

| Property | Value |
| --- | --- |
| Code | `date` |
| Built-in editors for the field | `date_picker` |
| Available validators | `required`, `date_range` |

</details>

<details>
<summary>Date time (date_time)</summary>

| Property | Value |
| --- | --- |
| Code | `date_time` |
| Built-in editors for the field | `date_time_picker` |
| Available validators | `required`, `date_time_range` |

</details>

<details>
<summary>Color (color)</summary>

| Property | Value |
| --- | --- |
| Code | `color` |
| Built-in editors for the field | `color_picker` |
| Available validators | `required` |

</details>

<details>
<summary>JSON (json)</summary>

| Property | Value |
| --- | --- |
| Code | `json` |
| Built-in editors for the field | `json`, `string_multi_select`, `string_checkbox_group` |
| Available validators | `required` |

</details>

<details>
<summary>Location (lat_lon)</summary>

| Property | Value |
| --- | --- |
| Code | `lat_lon` |
| Built-in editors for the field | `map` |
| Available validators | `required` |

</details>

<details>
<summary>SEO and Social (seo)</summary>

| Property | Value |
| --- | --- |
| Code | `seo` |
| Built-in editors for the field | `seo` |
| Available validators | `required_seo_fields`, `file_size`, `image_dimensions`, `image_aspect_ratio`, `title_length`, `description_length` |

</details>

<details>
<summary>Slug (slug)</summary>

| Property | Value |
| --- | --- |
| Code | `slug` |
| Built-in editors for the field | `slug` |
| Available validators | `required`, `unique`, `length`, `slug_format`, `slug_title_field` |

</details>

<details>
<summary>External video (video)</summary>

| Property | Value |
| --- | --- |
| Code | `video` |
| Built-in editors for the field | `video` |
| Available validators | `required` |

</details>

<details>
<summary>Single-asset (file)</summary>

| Property | Value |
| --- | --- |
| Code | `file` |
| Built-in editors for the field | `file` |
| Available validators | `required`, `file_size`, `image_dimensions`, `image_aspect_ratio`, `extension`, `required_alt_title` |

</details>

<details>
<summary>Asset gallery (gallery)</summary>

| Property | Value |
| --- | --- |
| Code | `gallery` |
| Built-in editors for the field | `gallery` |
| Available validators | `size`, `file_size`, `image_dimensions`, `image_aspect_ratio`, `extension`, `required_alt_title` |

</details>

<details>
<summary>Single link (link)</summary>

| Property | Value |
| --- | --- |
| Code | `link` |
| Built-in editors for the field | `link_select`, `link_embed` |
| Default `editor` | `link_select` |
| Required validators | `item_item_type` |
| Other validators available | `required`, `unique` |

</details>

<details>
<summary>Multiple links (links)</summary>

| Property | Value |
| --- | --- |
| Code | `links` |
| Built-in editors for the field | `links_select`, `links_embed` |
| Default `editor` | `links_select` |
| Required validators | `items_item_type` |
| Other validators available | `size` |

</details>

<details>
<summary>Modular content (rich_text)</summary>

| Property | Value |
| --- | --- |
| Code | `rich_text` |
| Built-in editors for the field | `rich_text` |
| Required validators | `rich_text_blocks` |
| Other validators available | `size` |

</details>

<details>
<summary>Single Block (single_block)</summary>

| Property | Value |
| --- | --- |
| Code | `single_block` |
| Built-in editors for the field | `framed_single_block`, `frameless_single_block` |
| Required validators | `single_block_blocks` |
| Other validators available | `required` |

</details>

<details>
<summary>Structured text (structured_text)</summary>

| Property | Value |
| --- | --- |
| Code | `structured_text` |
| Built-in editors for the field | `structured_text` |
| Required validators | `structured_text_blocks`, `structured_text_links` |
| Other validators available | `required`, `length`, `structured_text_inline_blocks` |

</details>

### Validators

<details>
<summary>date_range</summary>

Accept dates only inside a specified date range.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min` | ISO 8601 date |  | Minimum date |
| `max` | ISO 8601 date |  | Maximum date |

At least one of the parameters must be specified.

</details>

<details>
<summary>date_time_range</summary>

Accept date times only inside a specified date range.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min` | ISO 8601 datetime |  | Minimum datetime |
| `max` | ISO 8601 datetime |  | Maximum datetime |

At least one of the parameters must be specified.

</details>

<details>
<summary>enum</summary>

Only accept a specific set of values

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `values` | `Array<String>` | ✅ | Set of allowed values |

</details>

<details>
<summary>extension</summary>

Only accept assets with specific file extensions.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `extensions` | `Array<String>` |  | Set of allowed file extensions |
| `predefined_list` | one of `"image"`, `"transformable_image"`, `"video"`, `"document"` |  | Allowed file type |

Only one of the parameters must be specified.

</details>

<details>
<summary>file_size</summary>

Accept assets only inside a specified date range.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min_value` | `Integer` |  | Numeric value for minimum filesize |
| `min_unit` | one of `"B"`, `"KB"`, `"MB"` |  | Unit for minimum filesize |
| `max_value` | `Integer` |  | Numeric value for maximum filesize |
| `max_unit` | one of `"B"`, `"KB"`, `"MB"` |  | Unit for maximum filesize |

At least one couple of value/unit must be specified.

</details>

<details>
<summary>format</summary>

Accepts only strings that match a specified format.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `custom_pattern` | `Regexp` | Optional | Custom regular expression for validation |
| `predefined_pattern` | `"email"` or `"url"` | Optional | Specifies a pre-defined format (email or URL) |

**Note:** Only one of `custom_pattern` or `predefined_pattern` should be specified.

If `custom_pattern` is used, an additional `description` parameter can be provided to serve as a hint for the user. This hint offers a simple explanation of the expected pattern, such as `"The field must end with an 's'"`, instead of the default message like `"Field must match the pattern: /s$/"`.

</details>

<details>
<summary>slug_format</summary>

Only accept slugs having a specific format.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `custom_pattern` | `Regexp` |  | Regular expression to be validated |
| `predefined_pattern` | `"webpage_slug"` |  | Allowed format |

Only one of the parameters must be specified.

</details>

<details>
<summary>image_dimensions</summary>

Accept assets only within a specified height and width range.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `width_min_value` | `Integer` |  | Numeric value for minimum width |
| `width_max_value` | `Integer` |  | Numeric value for maximum height |
| `height_min_value` | `Integer` |  | Numeric value for minimum width |
| `height_max_value` | `Integer` |  | Numeric value for maximum height |

At least one pair of height/width parameters must be specified.

</details>

<details>
<summary>image_aspect_ratio</summary>

Accept assets only within a specified aspect ratio range.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min_ar_numerator` | `Integer` |  | Numerator part of the minimum aspect ratio |
| `min_ar_denominator` | `Integer` |  | Denominator part of the minimum aspect ratio |
| `eq_ar_numerator` | `Integer` |  | Numerator part for the required aspect ratio |
| `eq_ar_denominator` | `Integer` |  | Denominator part for the required aspect ratio |
| `max_ar_numerator` | `Integer` |  | Numerator part of the maximum aspect ratio |
| `max_ar_denominator` | `Integer` |  | Denominator part of the maximum aspect ratio |

At least one pair of numerator/denominator must be specified.

</details>

<details>
<summary>item_item_type</summary>

Only accept references to records of the specified models.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `item_types` | `Array<Model ID>` | ✅ | Set of allowed model IDs |
| `on_publish_with_unpublished_references_strategy` | `"fail"`, `"publish_references"` (default value: `"fail"`) |  | Strategy to apply when a publishing is requested and this field references some unpublished records |
| `on_reference_unpublish_strategy` | `"fail"`, `"unpublish"`, `"delete_references"` (default value: `"fail"`) |  | Strategy to apply when unpublishing is requested for a record referenced by this field |
| `on_reference_delete_strategy` | `"fail"`, `"delete_references"` (default value: `"delete_references"`) |  | Strategy to apply when deletion is requested for a record referenced by this field |

Possible values for `on_publish_with_unpublished_references_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"publish_references"`: Publish also the referenced records

Possible values for `on_reference_unpublish_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"unpublish"`: Unpublish also this record
-   `"delete_references"`: Try to remove the reference to the unpublished record (if the field has a `required` validation it will fail)

Possible values for `on_reference_delete_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"delete_references"`: Try to remove the reference to the deleted record (if the field has a `required` validation it will fail)

</details>

<details>
<summary>items_item_type</summary>

Only accept references to records of the specified models.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `item_types` | `Array<Model ID>` | ✅ | Set of allowed model IDs |
| `on_publish_with_unpublished_references_strategy` | `"fail"`, `"publish_references"` (default value: `"fail"`) |  | Strategy to apply when a publishing is requested and this field references some unpublished records |
| `on_reference_unpublish_strategy` | `"fail"`, `"unpublish"`, `"delete_references"` (default value: `"fail"`) |  | Strategy to apply when unpublishing is requested for a record referenced by this field |
| `on_reference_delete_strategy` | `"fail"`, `"delete_references"` (default value: `"delete_references"`) |  | Strategy to apply when deletion is requested for a record referenced by this field |

Possible values for `on_publish_with_unpublished_references_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"publish_references"`: Publish also the referenced records

Possible values for `on_reference_unpublish_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"unpublish"`: Unpublish also this record
-   `"delete_references"`: Try to remove the reference to the unpublished record (if the field has a `required` validation it will fail)

Possible values for `on_reference_delete_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"delete_references"`: Try to remove the reference to the deleted record (if the field has a `required` validation it will fail)

</details>

<details>
<summary>length</summary>

Accept strings only with a specified number of characters.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min` | `Integer` |  | Minimum length |
| `eq` | `Integer` |  | Expected length |
| `max` | `Integer` |  | Maximum length |

At least one parameter must be specified.

</details>

<details>
<summary>number_range</summary>

Accept numbers only inside a specified range.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min` | `Float` |  | Minimum value |
| `max` | `Float` |  | Maximum value |

At least one of the parameters must be specified.

</details>

<details>
<summary>required</summary>

Value must be specified or it won't be valid.

</details>

<details>
<summary>required_alt_title</summary>

Assets contained in the field are required to specify custom title or alternate text, or they won't be valid.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | `Boolean` |  | Whether the title for the asset must be specified |
| `alt` | `Boolean` |  | Whether the alternate text for the asset must be specified |

At least one of the parameters must be specified.

</details>

<details>
<summary>required_seo_fields</summary>

SEO field has to specify one or more properties, or it won't be valid.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | `Boolean` |  | Whether the meta title must be specified |
| `description` | `Boolean` |  | Whether the meta description must be specified |
| `image` | `Boolean` |  | Whether the social sharing image must be specified |
| `twitter_card` | `Boolean` |  | Whether the type of Twitter card must be specified |

At least one of the parameters must be specified.

</details>

<details>
<summary>title_length</summary>

Limits the length of the title for a SEO field. Search engines usually truncate title tags to 60 character so it is a good practice to keep the title around this length.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min` | `Integer` |  | Minimum value |
| `max` | `Integer` |  | Maximum value |

At least one of the parameters must be specified.

</details>

<details>
<summary>description_length</summary>

Limits the length of the description for a SEO field. Search engines usually truncate description tags to 160 character so it is a good practice to keep the description around this length.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min` | `Integer` |  | Minimum value |
| `max` | `Integer` |  | Maximum value |

At least one of the parameters must be specified.

</details>

<details>
<summary>rich_text_blocks</summary>

Only accept references to block records of the specified block models.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `item_types` | `Array<Block Model ID>` | ✅ | Set of allowed Block Model IDs |

</details>

<details>
<summary>single_block_blocks</summary>

Only accept references to block records of the specified block models.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `item_types` | `Array<Block Model ID>` | ✅ | Set of allowed Block Model IDs |

</details>

<details>
<summary>sanitized_html</summary>

Checks for the presence of malicious code in HTML fields: content is valid if no dangerous code is present.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `sanitize_before_validation` | `Boolean` | ✅ | Content is actively sanitized before applying the validation |

</details>

<details>
<summary>structured_text_blocks</summary>

Only accept references to block records of the specified block models.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `item_types` | `Array<Block Model ID>` | ✅ | Set of allowed Block Model IDs |

</details>

<details>
<summary>structured_text_inline_blocks</summary>

Only accept references to block records of the specified block models.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `item_types` | `Array<Block Model ID>` | ✅ | Set of allowed Block Model IDs |

</details>

<details>
<summary>structured_text_links</summary>

Only accept `itemLink` to `inlineItem` nodes for records of the specified models.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `item_types` | `Array<Model ID>` | ✅ | Set of allowed model IDs |
| `on_publish_with_unpublished_references_strategy` | `"fail"`, `"publish_references"` (default value: `"fail"`) |  | Strategy to apply when a publishing is requested and this field references some unpublished records |
| `on_reference_unpublish_strategy` | `"fail"`, `"unpublish"`, `"delete_references"` (default value: `"delete_references"`) |  | Strategy to apply when unpublishing is requested for a record referenced by this field |
| `on_reference_delete_strategy` | `"fail"`, `"delete_references"` (default value: `"delete_references"`) |  | Strategy to apply when deletion is requested for a record referenced by this field |

Possible values for `on_publish_with_unpublished_references_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"publish_references"`: Publish also the referenced records

Possible values for `on_reference_unpublish_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"unpublish"`: Unpublish also this record
-   `"delete_references"`: Try to remove the reference to the unpublished record (if the field has a `required` validation it will fail)

Possible values for `on_reference_delete_strategy`:

-   `"fail"`: Fail the operation and notify the user
-   `"delete_references"`: Try to remove the reference to the deleted record (if the field has a `required` validation it will fail)

</details>

<details>
<summary>size</summary>

Only accept a number of items within the specified range.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `min` | `Integer` |  | Minimum length |
| `eq` | `Integer` |  | Expected length |
| `max` | `Integer` |  | Maximum length |
| `multiple_of` | `Integer` |  | The number of items must be multiple of this value |

At least one parameter must be specified.

</details>

<details>
<summary>slug_title_field</summary>

Specifies the ID of the *Single-line string* field that will be used to generate the slug

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `title_field_id` | `Field ID` | ✅ | The field that will be used to generate the slug |

</details>

<details>
<summary>unique</summary>

The value must be unique across the whole collection of records.

</details>

### Configuration parameters for DatoCMS built-in field editors

If a field editor is not specified in this table, just pass an empty object `{}` as its configuration parameters.

<details>
<summary>boolean_radio_group</summary>

Radio group input for *boolean* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `positive_radio` | `{ label: string, hint?: string }` | ✅ | Radio input for positive choice (`true`) |
| `negative_radio` | `{ label: string, hint?: string }` | ✅ | Radio input for negative choice (`false`) |

</details>

<details>
<summary>string_radio_group</summary>

Radio group input for *string* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `radios` | `Array<{ label: string, value: string, hint?: string }>` | ✅ | The different radio options |

</details>

<details>
<summary>string_select</summary>

Select input for *string* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `options` | `Array<{ label: string, value: string, hint?: string }>` | ✅ | The different select options |

</details>

<details>
<summary>string_multi_select</summary>

Select input for *JSON* fields, to edit an array of strings.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `options` | `Array<{ label: string, value: string, hint?: string }>` | ✅ | The different select options |

</details>

<details>
<summary>string_checkbox_group</summary>

Multiple chechboxes input for *JSON* fields, to edit an array of strings.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `options` | `Array<{ label: string, value: string, hint?: string }>` | ✅ | The different select options |

</details>

<details>
<summary>single_line</summary>

Simple textual input for *Single-line string* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `heading` | `Boolean` | ✅ | Indicates if the field should be shown bigger, as a field representing a heading |
| `placeholder` | `String` |  | A placeholder that will be shown in the editor's input to provide editors with an example. |

</details>

<details>
<summary>markdown</summary>

Markdown editor for *Multiple-paragraph text* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `toolbar` | `Array<String>` | ✅ | Specify which buttons the toolbar should have. Valid values: `"heading"`, `"bold"`, `"italic"`, `"strikethrough"`, `"code"`, `"unordered_list"`, `"ordered_list"`, `"quote"`, `"link"`, `"image"`, `"fullscreen"` |

</details>

<details>
<summary>wysiwyg</summary>

HTML editor for *Multiple-paragraph text* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `toolbar` | `Array<String>` | ✅ | Specify which buttons the toolbar should have. Valid values: `"format"`, `"bold"`, `"italic"`, `"strikethrough"`, `"code"`, `"ordered_list"`, `"unordered_list"`, `"quote"`, `"table"`, `"link"`, `"image"`, `"show_source"`, `"undo"`, `"redo"`, `"align_left"`, `"align_center"`, `"align_right"`, `"align_justify"`, `"outdent"`, `"indent"`, `"fullscreen"` |

</details>

<details>
<summary>textarea</summary>

Basic textarea editor for *Multiple-paragraph text* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `placeholder` | `String` |  | A placeholder that will be shown in the editor's input to provide editors with an example. |

</details>

<details>
<summary>color_picker</summary>

Built-in editor for *Color* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `enable_alpha` | `Boolean` | ✅ | Should the color picker allow to specify the alpha value? |
| `preset_colors` | `Array<Hex color string>` | ✅ | List of preset colors to offer to the user |

</details>

<details>
<summary>slug</summary>

Built-in editor for *Slug* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `url_prefix` | `String` |  | A prefix that will be shown in the editor's form to give some context to your editors. |
| `placeholder` | `String` |  | A placeholder that will be shown in the editor's input to provide editors with an example. |

</details>

<details>
<summary>seo</summary>

Built-in editor for *seo* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `fields` | `Array<String>` | ✅ | Specify which fields of the SEO input should be visible to editors. Valid values: `"title"`, `"description"`, `"image"`, `"no_index"`, `"twitter_card"` |
| `previews` | `Array<String>` | ✅ | Specify which previews should be visible to editors. Valid values: `"google"`, `"twitter"`, `"slack"`, `"whatsapp"`, `"telegram"`, `"facebook"`, `"linkedin"` |

</details>

<details>
<summary>rich_text</summary>

Built-in editor for *Modular content* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `start_collapsed` | `Boolean` |  | Whether you want block records collapsed by default or not |

</details>

<details>
<summary>framed_single_block</summary>

Built-in editor for *Single block* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `start_collapsed` | `Boolean` |  | Whether you want block record collapsed by default or not |

</details>

<details>
<summary>structured_text</summary>

Built-in editor for *Structured text* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `nodes` | `Array<String>` | ✅ | Specify which nodes the field should allow. Valid values: `"blockquote"`, `"code"`, `"heading"`, `"link"`, `"list"`, `"thematicBreak"` |
| `marks` | `Array<String>` | ✅ | Specify which marks the field should allow. Valid values: `"strong"`, `"emphasis"`, `"underline"`, `"strikethrough"`, `"code"`, `"highlight"` |
| `heading_levels` | `Array<Integer>` | ✅ | If `nodes` includes `"heading"`, specify which heading levels the field should allow. Valid values: numbers between 1 and 6 |
| `blocks_start_collapsed` | `Boolean` |  | Whether you want block nodes collapsed by default or not |
| `show_links_target_blank` | `Boolean` |  | Whether you want to show the "Open this link in a new tab?" checkbox, that fills in the `target: "_blank"` meta attribute for links |
| `show_links_meta_editor` | `Boolean` |  | Whether you want to show the complete meta editor for links |

</details>

<details>
<summary>link_select and links_select</summary>

Use a select input with auto-completion to pick the records to reference inside the field.

</details>

<details>
<summary>link_embed and links_embed</summary>

Use an expanded view with records' image preview to pick the records to reference inside the field.

</details>

<details>
<summary>integer</summary>

Built-in editor for *Integer* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `placeholder` | `String` |  | A placeholder that will be shown in the editor's input to provide editors with an example. |

</details>

<details>
<summary>float</summary>

Built-in editor for *Float* fields.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `placeholder` | `String` |  | A placeholder that will be shown in the editor's input to provide editors with an example. |

</details>

## Object payload

**`id`**

- Type: string
- Example: `"Pkg-oztERp6o-Rj76nYKJg"`

RFC 4122 UUID of field expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"field"`.

**`label`**

- Type: string
- Example: `"Title"`

The label of the field

**`field_type`**

- Type: enum
- Example: `"string"`

Type of input

<details>
<summary>Show enum values</summary>

**`boolean`**

**`color`**

**`date`**

**`date_time`**

**`file`**

**`float`**

**`gallery`**

**`integer`**

**`json`**

**`lat_lon`**

**`link`**

**`links`**

**`rich_text`**

**`seo`**

**`single_block`**

**`slug`**

**`string`**

**`structured_text`**

**`text`**

**`video`**

</details>

**`api_key`**

- Type: string
- Example: `"title"`

Field API key

**`localized`**

- Type: boolean

Whether the field needs to be multilanguage or not

**`validators`**

- Type: object
- Example: `{ required: {} }`

Optional field validations

**`position`**

- Type: integer
- Example: `1`

Ordering index

**`hint`**

- Type: string, null
- Example: `"This field will be used as post title"`

Field hint

**`default_value`**

- Type: boolean, null, string, number, object
- Example: `{ en: "A default value", it: "Un valore di default" }`

Default value for Field. When field is localized accepts an object of default values with site locales as keys

**`appearance`**

- Type: object

Field appearance details, plugin configuration and field add-ons

Example:

```json
{
  editor: "single_line",
  parameters: { heading: false },
  addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
}
```

<details>
<summary>Show object format</summary>

**`editor`**

- Type: string

A valid editor can be a DatoCMS default field editor type (ie. `"single_line"`), or a plugin ID offering a custom field editor

**`parameters`**

- Type: object

The editor plugin's parameters

**`addons`**

- Type: Array\<object\>

An array of add-on plugins with id and parameters

<details>
<summary>Show objects format inside array</summary>

**`id`**

- Type: string

The ID of a plugin offering a field addon

**`parameters`**

- Type: object

**`field_extension`**

- Type: string

The specific field extension to use for the field (only if the editor is a modern plugin)

</details>

**`field_extension`**

- Type: string

The specific field extension to use for the field (only if the editor is a modern plugin)

</details>

**`deep_filtering_enabled`**

- Type: boolean

Whether deep filtering for block models is enabled in GraphQL or not

**`content_link_enabled`**

- Type: boolean

Whether Content Link (visual editing) encoding is emitted for this field's value in the Content Delivery API. Defaults to `true`; can only be set to `false` on `string`, `text` and `structured_text` fields

**`item_type`**

- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

Field item type

**`fieldset`**

- Type: null, [ResourceLinkage\<"fieldset"\>](https://www-draft.datocms.com/docs/content-management-api/resources/fieldset.md)

Fieldset linkage

<details>
<summary>Show deprecated</summary>

**`appeareance`**

- Deprecated
- Type: object

Field appearance

This field contains a typo and will be removed in future versions: use `appearance` instead

<details>
<summary>Show object format</summary>

**`editor`**

- Type: string

**`parameters`**

- Type: object

</details>

</details>

---

# Content Management API — Create a new field

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"Pkg-oztERp6o-Rj76nYKJg"`

RFC 4122 UUID of field expressed in URL-safe base64 format

**`label`**

- Required
- Type: string
- Example: `"Title"`

The label of the field

**`field_type`**

- Required
- Type: enum
- Example: `"string"`

Type of input

<details>
<summary>Show enum values</summary>

**`boolean`**

- Optional

**`color`**

- Optional

**`date`**

- Optional

**`date_time`**

- Optional

**`file`**

- Optional

**`float`**

- Optional

**`gallery`**

- Optional

**`integer`**

- Optional

**`json`**

- Optional

**`lat_lon`**

- Optional

**`link`**

- Optional

**`links`**

- Optional

**`rich_text`**

- Optional

**`seo`**

- Optional

**`single_block`**

- Optional

**`slug`**

- Optional

**`string`**

- Optional

**`structured_text`**

- Optional

**`text`**

- Optional

**`video`**

- Optional

</details>

**`api_key`**

- Required
- Type: string
- Example: `"title"`

Field API key

**`localized`**

- Optional
- Type: boolean

Whether the field needs to be multilanguage or not

**`validators`**

- Optional
- Type: object
- Example: `{ required: {} }`

Optional field validations

**`appearance`**

- Optional
- Type: object

Field appearance details, plugin configuration and field add-ons

Example:

```json
{
  editor: "single_line",
  parameters: { heading: false },
  addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
}
```

<details>
<summary>Show object format</summary>

**`editor`**

- Required
- Type: string

A valid editor can be a DatoCMS default field editor type (ie. `"single_line"`), or a plugin ID offering a custom field editor

**`parameters`**

- Required
- Type: object

The editor plugin's parameters

**`addons`**

- Required
- Type: Array\<object\>

An array of add-on plugins with id and parameters

<details>
<summary>Show objects format inside array</summary>

**`id`**

- Required
- Type: string

The ID of a plugin offering a field addon

**`parameters`**

- Required
- Type: object

**`field_extension`**

- Optional
- Type: string

The specific field extension to use for the field (only if the editor is a modern plugin)

</details>

**`field_extension`**

- Optional
- Type: string

The specific field extension to use for the field (only if the editor is a modern plugin)

</details>

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`hint`**

- Optional
- Type: string, null
- Example: `"This field will be used as post title"`

Field hint

**`default_value`**

- Optional
- Type: boolean, null, string, number, object
- Example: `{ en: "A default value", it: "Un valore di default" }`

Default value for Field. When field is localized accepts an object of default values with site locales as keys

**`deep_filtering_enabled`**

- Optional
- Type: boolean

Whether deep filtering for block models is enabled in GraphQL or not

**`content_link_enabled`**

- Optional
- Type: boolean

Whether Content Link (visual editing) encoding is emitted for this field's value in the Content Delivery API. Defaults to `true`; can only be set to `false` on `string`, `text` and `structured_text` fields

**`fieldset`**

- Optional
- Type: null, [ResourceLinkage\<"fieldset"\>](https://www-draft.datocms.com/docs/content-management-api/resources/fieldset.md)

Fieldset linkage

<details>
<summary>Show deprecated</summary>

**`appeareance`**

- Deprecated
- Type: object

Field appearance

This field contains a typo and will be removed in future versions: use `appearance` instead

<details>
<summary>Show object format</summary>

**`editor`**

- Required
- Type: string

**`parameters`**

- Required
- Type: object

</details>

</details>

## Returns

Returns a resource object of type [field](/docs/content-management-api/resources/field.md)

## Other examples

###### Example Basic example

This is a complete example for creating a new localized *Single-line string* field:

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const field = await client.fields.create(modelIdOrApiKey, {
    label: "Title",
    field_type: "string",
    api_key: "title",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(field);
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```


###### Example Creating Modular Content fields

In this example:

-   first we create some [block models](/docs/content-modelling/blocks.md) using the `client.itemTypes.create()` method, making sure to set the `modular_block` attribute to `true` — this tells the API that they're in fact block models, and not regular models;
-   we then create a [Modular content](/docs/content-modelling/modular-content.md) field, passing down the allowed block models in the `rich_text_blocks` validator:

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modularBlock1 = await client.itemTypes.create({
    name: "Modular Block 1",
    api_key: "modular_block1",
    modular_block: true,
  });

  const modularBlock2 = await client.itemTypes.create({
    name: "Modular Block 2",
    api_key: "modular_block2",
    modular_block: true,
  });

  const field = await client.fields.create("UZyfjdBES8y2W2ruMEHSoA", {
    label: "Content",
    field_type: "rich_text",
    api_key: "content",
    validators: {
      rich_text_blocks: {
        item_types: [modularBlock1.id, modularBlock2.id],
      },
    },
  });

  console.log(field);
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```


###### Example Creating Structured Text fields

[Structured Text](/docs/content-modelling/structured-text.md) fields support both embedded block records and links to other regular records.

For DatoCMS, a block model is just like a regular model, so we'll create them with `client.itemTypes.create()`, passing the `modularBlock` property to `true`:

In this example:

-   first we create some [block models](/docs/content-modelling/blocks.md) using the `client.itemTypes.create()` method, making sure to set the `modular_block` attribute to `true` — this tells the API that they're in fact block models, and not regular models;
-   we then create the Structured Text field, passing down the embeddable block models in the `structured_text_blocks` and `structured_text_inline_blocks` validator, and the linkable record models in the `structured_text_links` validator:

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modularBlock1 = await client.itemTypes.create({
    name: "Modular Block 1",
    api_key: "modular_block1",
    modular_block: true,
  });

  const modularBlock2 = await client.itemTypes.create({
    name: "Modular Block 2",
    api_key: "modular_block2",
    modular_block: true,
  });

  const field = await client.fields.create("UZyfjdBES8y2W2ruMEHSoA", {
    label: "Structured content",
    field_type: "structured_text",
    api_key: "content",
    validators: {
      structured_text_blocks: {
        item_types: [modularBlock1.id, modularBlock2.id],
      },
      structured_text_inline_blocks: {
        item_types: [modularBlock1.id],
      },
      structured_text_links: {
        item_types: ["UZyfjdBES8y2W2ruMEHSoA"],
      },
    },
  });

  console.log(field);
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — Update a field

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field/update.md

## Body parameters

**`default_value`**

- Optional
- Type: boolean, null, string, number, object
- Example: `{ en: "A default value", it: "Un valore di default" }`

Default value for Field. When field is localized accepts an object of default values with site locales as keys

**`label`**

- Optional
- Type: string
- Example: `"Title"`

The label of the field

**`api_key`**

- Optional
- Type: string
- Example: `"title"`

Field API key

**`localized`**

- Optional
- Type: boolean

Whether the field needs to be multilanguage or not

**`validators`**

- Optional
- Type: object
- Example: `{ required: {} }`

Optional field validations

**`appearance`**

- Optional
- Type: object

Field appearance details, plugin configuration and field add-ons

Example:

```json
{
  editor: "single_line",
  parameters: { heading: false },
  addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
}
```

<details>
<summary>Show object format</summary>

**`editor`**

- Required
- Type: string

A valid editor can be a DatoCMS default field editor type (ie. `"single_line"`), or a plugin ID offering a custom field editor

**`parameters`**

- Required
- Type: object

The editor plugin's parameters

**`addons`**

- Required
- Type: Array\<object\>

An array of add-on plugins with id and parameters

<details>
<summary>Show objects format inside array</summary>

**`id`**

- Required
- Type: string

The ID of a plugin offering a field addon

**`parameters`**

- Required
- Type: object

**`field_extension`**

- Optional
- Type: string

The specific field extension to use for the field (only if the editor is a modern plugin)

</details>

**`field_extension`**

- Optional
- Type: string

The specific field extension to use for the field (only if the editor is a modern plugin)

</details>

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`field_type`**

- Optional
- Type: enum
- Example: `"string"`

Type of input

<details>
<summary>Show enum values</summary>

**`boolean`**

- Optional

**`color`**

- Optional

**`date`**

- Optional

**`date_time`**

- Optional

**`file`**

- Optional

**`float`**

- Optional

**`gallery`**

- Optional

**`integer`**

- Optional

**`json`**

- Optional

**`lat_lon`**

- Optional

**`link`**

- Optional

**`links`**

- Optional

**`rich_text`**

- Optional

**`seo`**

- Optional

**`single_block`**

- Optional

**`slug`**

- Optional

**`string`**

- Optional

**`structured_text`**

- Optional

**`text`**

- Optional

**`video`**

- Optional

</details>

**`hint`**

- Optional
- Type: string, null
- Example: `"This field will be used as post title"`

Field hint

**`deep_filtering_enabled`**

- Optional
- Type: boolean

Whether deep filtering for block models is enabled in GraphQL or not

**`content_link_enabled`**

- Optional
- Type: boolean

Whether Content Link (visual editing) encoding is emitted for this field's value in the Content Delivery API. Defaults to `true`; can only be set to `false` on `string`, `text` and `structured_text` fields

**`fieldset`**

- Optional
- Type: null, [ResourceLinkage\<"fieldset"\>](https://www-draft.datocms.com/docs/content-management-api/resources/fieldset.md)

Fieldset linkage

<details>
<summary>Show deprecated</summary>

**`appeareance`**

- Deprecated
- Type: object

Field appearance

This field contains a typo and will be removed in future versions: use `appearance` instead

<details>
<summary>Show object format</summary>

**`editor`**

- Required
- Type: string

**`parameters`**

- Required
- Type: object

</details>

</details>

## Returns

Returns a resource object of type [field](/docs/content-management-api/resources/field.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const fieldIdOrApiKey = "blog_post::title";

  const field = await client.fields.update(fieldIdOrApiKey, {
    id: "Pkg-oztERp6o-Rj76nYKJg",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(field);
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — List all fields of a model/block

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field/instances.md

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`field`**

- Type: string
- Example: `"label,field_type,localized,item_type"`

Attributes and relationships to return for each `field` that this endpoint returns.

</details>

## Returns

Returns an array of resource objects of type [field](/docs/content-management-api/resources/field.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const fields = await client.fields.list(modelIdOrApiKey);

  for (const field of fields) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(field);
  }
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — List fields referencing a model/block

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field/referencing.md

Returns all fields in the project that reference a specific model (works both for Models and Block Models)

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`field`**

- Type: string
- Example: `"label,field_type,localized,item_type"`

Attributes and relationships to return for each `field` that this endpoint returns.

</details>

## Returns

Returns an array of resource objects of type [field](/docs/content-management-api/resources/field.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const fields = await client.fields.referencing(modelIdOrApiKey);

  for (const field of fields) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(field);
  }
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — Retrieve a field

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field/self.md

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`field`**

- Type: string
- Example: `"label,field_type,localized,item_type"`

Attributes and relationships to return for each `field` that this endpoint returns.

</details>

## Returns

Returns a resource object of type [field](/docs/content-management-api/resources/field.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const fieldIdOrApiKey = "blog_post::title";

  const field = await client.fields.find(fieldIdOrApiKey);

  // Check the 'Returned output' tab for the result ☝️
  console.log(field);
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — Delete a field

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field/destroy.md

## Returns

Returns a resource object of type [field](/docs/content-management-api/resources/field.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const fieldIdOrApiKey = "blog_post::title";

  const field = await client.fields.destroy(fieldIdOrApiKey);

  // Check the 'Returned output' tab for the result ☝️
  console.log(field);
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — Duplicate a field

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/field/duplicate.md

## Returns

Returns a resource object of type [field](/docs/content-management-api/resources/field.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const fieldIdOrApiKey = "blog_post::title";

  const field = await client.fields.duplicate(fieldIdOrApiKey);

  // Check the 'Returned output' tab for the result ☝️
  console.log(field);
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — Fieldset

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/fieldset.md

Fields can be organized and grouped into fieldset to better present them to editors.

## Object payload

**`id`**

- Type: string
- Example: `"93Y1C2sySkG4Eg0atBRIwg"`

RFC 4122 UUID of fieldset expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"fieldset"`.

**`title`**

- Type: string
- Example: `"SEO-related fields"`

The title of the fieldset

**`hint`**

- Type: string, null
- Example: `"Please fill in these fields!"`

Description/contextual hint for the fieldset

**`position`**

- Type: integer
- Example: `1`

Ordering index

**`collapsible`**

- Type: boolean

Whether the fieldset can be collapsed or not

**`start_collapsed`**

- Type: boolean

When fieldset is collapsible, determines if the default is to start collapsed or not

**`item_type`**

- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

Fieldset item type

---

# Content Management API — Create a new fieldset

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/fieldset/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"93Y1C2sySkG4Eg0atBRIwg"`

RFC 4122 UUID of fieldset expressed in URL-safe base64 format

**`title`**

- Required
- Type: string
- Example: `"SEO-related fields"`

The title of the fieldset

**`hint`**

- Optional
- Type: string, null
- Example: `"Please fill in these fields!"`

Description/contextual hint for the fieldset

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`collapsible`**

- Optional
- Type: boolean

Whether the fieldset can be collapsed or not

**`start_collapsed`**

- Optional
- Type: boolean

When fieldset is collapsible, determines if the default is to start collapsed or not

## Returns

Returns a resource object of type [fieldset](/docs/content-management-api/resources/fieldset.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const fieldset = await client.fieldsets.create(modelIdOrApiKey, {
    title: "SEO-related fields",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(fieldset);
}

run();
```

Returned output

```javascript
{
  id: "93Y1C2sySkG4Eg0atBRIwg",
  title: "SEO-related fields",
  hint: "Please fill in these fields!",
  position: 1,
  collapsible: true,
  start_collapsed: false,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Update a fieldset

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/fieldset/update.md

## Body parameters

**`title`**

- Optional
- Type: string
- Example: `"SEO-related fields"`

The title of the fieldset

**`hint`**

- Optional
- Type: string, null
- Example: `"Please fill in these fields!"`

Description/contextual hint for the fieldset

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`collapsible`**

- Optional
- Type: boolean

Whether the fieldset can be collapsed or not

**`start_collapsed`**

- Optional
- Type: boolean

When fieldset is collapsible, determines if the default is to start collapsed or not

## Returns

Returns a resource object of type [fieldset](/docs/content-management-api/resources/fieldset.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const fieldsetId = "93Y1C2sySkG4Eg0atBRIwg";

  const fieldset = await client.fieldsets.update(fieldsetId, {
    id: "93Y1C2sySkG4Eg0atBRIwg",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(fieldset);
}

run();
```

Returned output

```javascript
{
  id: "93Y1C2sySkG4Eg0atBRIwg",
  title: "SEO-related fields",
  hint: "Please fill in these fields!",
  position: 1,
  collapsible: true,
  start_collapsed: false,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — List all fieldsets of a model/block

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/fieldset/instances.md

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`fieldset`**

- Type: string
- Example: `"title,hint,collapsible,item_type"`

Attributes and relationships to return for each `fieldset` that this endpoint returns.

</details>

## Returns

Returns an array of resource objects of type [fieldset](/docs/content-management-api/resources/fieldset.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const modelIdOrApiKey = "blog_post";

  const fieldsets = await client.fieldsets.list(modelIdOrApiKey);

  for (const fieldset of fieldsets) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(fieldset);
  }
}

run();
```

Returned output

```javascript
{
  id: "93Y1C2sySkG4Eg0atBRIwg",
  title: "SEO-related fields",
  hint: "Please fill in these fields!",
  position: 1,
  collapsible: true,
  start_collapsed: false,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Retrieve a fieldset

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/fieldset/self.md

## Query parameters

**`fields`**

- Type: object

Sparse fieldsets: for each entity type, the attributes and the relationships to return, separated by commas. Each entity declares its attributes and its relationships in its own schema.

<details>
<summary>Show object format</summary>

**`fieldset`**

- Type: string
- Example: `"title,hint,collapsible,item_type"`

Attributes and relationships to return for each `fieldset` that this endpoint returns.

</details>

## Returns

Returns a resource object of type [fieldset](/docs/content-management-api/resources/fieldset.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const fieldsetId = "93Y1C2sySkG4Eg0atBRIwg";

  const fieldset = await client.fieldsets.find(fieldsetId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(fieldset);
}

run();
```

Returned output

```javascript
{
  id: "93Y1C2sySkG4Eg0atBRIwg",
  title: "SEO-related fields",
  hint: "Please fill in these fields!",
  position: 1,
  collapsible: true,
  start_collapsed: false,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Delete a fieldset

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/fieldset/destroy.md

## Returns

Returns a resource object of type [fieldset](/docs/content-management-api/resources/fieldset.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const fieldsetId = "93Y1C2sySkG4Eg0atBRIwg";

  const fieldset = await client.fieldsets.destroy(fieldsetId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(fieldset);
}

run();
```

Returned output

```javascript
{
  id: "93Y1C2sySkG4Eg0atBRIwg",
  title: "SEO-related fields",
  hint: "Please fill in these fields!",
  position: 1,
  collapsible: true,
  start_collapsed: false,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Record version

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-version.md

Every change to a record is stored as a separate record version in DatoCMS.

## Object payload

**`id`**

- Type: string
- Example: `"59JSonvYTCOUDz_b7_6hvA"`

RFC 4122 UUID of redord version expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"item_version"`.

**`meta.created_at`**

- Type: date-time

Date of record version creation

**`meta.is_published`**

- Type: boolean

Whether the record version is the published version or not

**`meta.published_from`**

- Type: date-time, null

Date this version became the published version of the record, or `null` if it has never been published. May also be `null` for versions that were published before publication-history tracking was introduced; in that case, fall back to `is_published` to determine the live status.

**`meta.published_until`**

- Type: date-time, null

Date this version stopped being the published version of the record (either replaced by a newer published version, or explicitly unpublished). `null` when the version is currently published or has never been published.

**`meta.is_current`**

- Type: boolean

Whether the record version is the most recent version or not

**`item_type`**

- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

The record version's model

**`item`**

- Type: [ResourceLinkage\<"item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item.md)

The record this version belongs to

**`editor`**

- Type: [ResourceLinkage\<"account"\>](https://www-draft.datocms.com/docs/content-management-api/resources/account.md), [ResourceLinkage\<"access_token"\>](https://www-draft.datocms.com/docs/content-management-api/resources/access_token.md), [ResourceLinkage\<"user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/user.md), [ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md), [ResourceLinkage\<"organization"\>](https://www-draft.datocms.com/docs/content-management-api/resources/organization.md)

The entity (account/collaborator/access token/sso user) who made this change to the record

<details>
<summary>Show deprecated</summary>

**`meta.is_valid`**

- Deprecated
- Type: boolean

Whether the record version is valid or not

Validity of a version can only be established in the context of all the current or published item's versions (think about uniqueness validations, for example): use item's `is_current_version_valid` or `is_published_version_valid` fields instead.

</details>

---

# Content Management API — Restore an old record version

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-version/restore.md

## Returns

Returns an array of resource objects of type [item](/docs/content-management-api/resources/item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemVersionId = "59JSonvYTCOUDz_b7_6hvA";

  const itemVersion = await client.itemVersions.restore(itemVersionId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemVersion);
}

run();
```

Returned output

```javascript
{
  id: "hWl-mnkWRYmMCSTq4z_piQ",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    updated_at: "2020-04-21T07:57:11.124Z",
    published_at: "2020-04-21T07:57:11.124Z",
    first_published_at: "2020-04-21T07:57:11.124Z",
    publication_scheduled_at: "2020-04-21T07:57:11.124Z",
    unpublishing_scheduled_at: "2020-04-21T07:57:11.124Z",
    status: "published",
    is_current_version_valid: true,
    is_published_version_valid: true,
    current_version: "4234",
    stage: null,
    has_children: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — List all record versions

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-version/instances.md

## Query parameters

**`nested`**

- Type: boolean

For Modular Content, Structured Text and Single Block fields. If set, returns full payload for nested blocks instead of IDs

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer
- Example: `200`

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 15, maximum is 50)

</details>

## Returns

Returns an array of resource objects of type [item\_version](/docs/content-management-api/resources/item-version.md)

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemId = "59JSonvYTCOUDz_b7_6hvA";

  // iterates over every page of results
  for await (const itemVersion of client.itemVersions.listPagedIterator(
    itemId,
  )) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(itemVersion);
  }
}

run();
```

---

# Content Management API — Retrieve a record version

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-version/self.md

## Query parameters

**`nested`**

- Type: boolean

For Modular Content, Structured Text and Single Block fields, return full payload for nested blocks instead of IDs

## Returns

Returns a resource object of type [item\_version](/docs/content-management-api/resources/item-version.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemVersionId = "59JSonvYTCOUDz_b7_6hvA";

  const itemVersion = await client.itemVersions.find(itemVersionId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemVersion);
}

run();
```

Returned output

```javascript
{
  id: "59JSonvYTCOUDz_b7_6hvA",
  title: "My first blog post!",
  content: "Lorem ipsum dolor sit amet...",
  category: "24",
  image: {
    alt: "Alt text",
    title: "Image title",
    custom_data: {},
    focal_point: null,
    upload_id: "20042921",
  },
  meta: {
    created_at: "2020-04-21T07:57:11.124Z",
    is_published: true,
    published_from: "2020-04-21T07:57:11.124Z",
    published_until: "2020-04-21T07:57:11.124Z",
    is_current: true,
  },
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  item: { type: "item", id: "hWl-mnkWRYmMCSTq4z_piQ" },
  editor: { type: "account", id: "312" },
}
```

---

# Content Management API — Upload permission

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-request.md

To upload a file with the Content Management API, first you need to obtain an upload permission. The `upload_request` entity contains the S3-like URL where you will be able to upload the file with a raw/binary PUT request.

## Object payload

**`id`**

- Type: string
- Example: `"/7/1455102967-image.png"`

The S3 path where the file will be stored

**`type`**

- Type: string

Must be exactly `"upload_request"`.

**`url`**

- Type: string
- Example: `"https://dato-images.s3-eu-west-1.amazonaws.com/7/1455102967-image.png?X-Amz-Credential=AKIAJDTXTZHHDUCKAUMA%2F20160210"`

The URL to use to upload the file with a raw/binary PUT request

**`request_headers`**

- Type: object

Specifies the additional headers that need to be included in the direct PUT upload request

---

# Content Management API — Request a new permission to upload a file

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-request/create.md

⚠️ **We highly advocate for [utilizing our JavaScript client when uploading new assets](/docs/content-management-api/resources/upload/create.md)**, as the `client.upload` resource comes equipped with high-level helper methods that handle all the nitty-gritty for you.

This endpoint is required to acquire the S3-like URL where you can upload a file using a raw/binary PUT request.

## Body parameters

**`filename`**

- Optional
- Type: string
- Example: `"image.png"`

The original file name

**`upload_collection`**

- Optional
- Type: [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md), null

Upload collection to which the asset belongs

## Returns

Returns a resource object of type [upload\_request](/docs/content-management-api/resources/upload-request.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadRequest = await client.uploadRequest.create({});

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadRequest);
}

run();
```

Returned output

```javascript
{
  id: "/7/1455102967-image.png",
  url: "https://dato-images.s3-eu-west-1.amazonaws.com/7/1455102967-image.png?X-Amz-Credential=AKIAJDTXTZHHDUCKAUMA%2F20160210",
  request_headers: {},
}
```

---

# Content Management API — Upload track

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-track.md

If the asset linked to an Upload entity is a video file, you have the option to include additional audio tracks and subtitle tracks to it.

## Object payload

**`id`**

- Type: string
- Example: `"xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw"`

ID of the upload track

**`type`**

- Type: enum
- Example: `"subtitles"`

The type of track (audio or subtitles)

<details>
<summary>Show enum values</summary>

**`subtitles`**

Subtitles

**`audio`**

Audio

</details>

**`name`**

- Type: string
- Example: `"Italiano"`

The human-readable name of the track

**`language_code`**

- Type: string
- Example: `"it-IT"`

A valid BCP 47 specification compliant language code

**`closed_captions`**

- Type: null, boolean

Indicates if the track provides subtitles for the Deaf or Hard-of-hearing (SDH)

**`status`**

- Type: enum
- Example: `"ready"`

The status of the asset

<details>
<summary>Show enum values</summary>

**`preparing`**

Preparing

**`ready`**

Ready

**`errored`**

Errored

</details>

**`error`**

- Type: null, string

When status is `errored`, explains the reason for the error

**`upload`**

- Type: [ResourceLinkage\<"upload"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload.md)

The upload containing the track

---

# Content Management API — Create a new upload track

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-track/create.md

## Body parameters

**`url_or_upload_request_id`**

- Required
- Type: string
- Example: `"/7/1455102967-image.png"`

Either an URL to download, or the ID of an upload request

**`type`**

- Required
- Type: enum
- Example: `"subtitles"`

The type of track (audio or subtitles)

<details>
<summary>Show enum values</summary>

**`subtitles`**

- Optional

Subtitles

**`audio`**

- Optional

Audio

</details>

**`language_code`**

- Required
- Type: string
- Example: `"it-IT"`

A valid BCP 47 specification compliant language code

**`name`**

- Optional
- Type: string
- Example: `"Italiano"`

The human-readable name of the track

**`closed_captions`**

- Optional
- Type: null, boolean

Indicates if the track provides subtitles for the Deaf or Hard-of-hearing (SDH)

## Returns

Returns a resource object of type [upload\_track](/docs/content-management-api/resources/upload-track.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw";

  const uploadTrack = await client.uploadTracks.create(uploadId, {
    url_or_upload_request_id: "/7/1455102967-image.png",
    type: "subtitles",
    language_code: "it-IT",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadTrack);
}

run();
```

Returned output

```javascript
{
  id: "xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw",
  type: "subtitles",
  name: "Italiano",
  language_code: "it-IT",
  closed_captions: false,
  status: "ready",
  error: null,
  upload: { type: "upload", id: "q0VNpiNQSkG6z0lif_O1zg" },
}
```

---

# Content Management API — List upload tracks

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-track/instances.md

## Returns

Returns an array of resource objects of type [upload\_track](/docs/content-management-api/resources/upload-track.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw";

  const uploadTracks = await client.uploadTracks.list(uploadId);

  for (const uploadTrack of uploadTracks) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(uploadTrack);
  }
}

run();
```

Returned output

```javascript
{
  id: "xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw",
  type: "subtitles",
  name: "Italiano",
  language_code: "it-IT",
  closed_captions: false,
  status: "ready",
  error: null,
  upload: { type: "upload", id: "q0VNpiNQSkG6z0lif_O1zg" },
}
```

---

# Content Management API — Delete an upload track

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-track/destroy.md

## Returns

Returns a resource object of type [upload\_track](/docs/content-management-api/resources/upload-track.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadId = "xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw";
  const uploadTrackId =
    "xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw";

  const uploadTrack = await client.uploadTracks.destroy(
    uploadId,
    uploadTrackId,
  );

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadTrack);
}

run();
```

Returned output

```javascript
{
  id: "xBe7u01029ipxBLQhYzZCJ1cke01zCkuUsgnYtH0017nNzbpv2YcsoMDmw",
  type: "subtitles",
  name: "Italiano",
  language_code: "it-IT",
  closed_captions: false,
  status: "ready",
  error: null,
  upload: { type: "upload", id: "q0VNpiNQSkG6z0lif_O1zg" },
}
```

---

# Content Management API — Manual tags

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-tag.md

All the project's upload tags

## Object payload

**`id`**

- Type: string
- Example: `"42"`

ID of upload tag

**`type`**

- Type: string

Must be exactly `"upload_tag"`.

**`name`**

- Type: string
- Example: `"Pictures of me"`

The tag name

---

# Content Management API — List all manually created upload tags

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-tag/instances.md

The results are sorted by name and paginated by default.

## Query parameters

**`filter`**

- Type: object

Attributes to filter tags

<details>
<summary>Show object format</summary>

**`query`**

- Type: string
- Example: `"foobar"`

Textual query to match.

</details>

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer
- Example: `200`

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 50, maximum is 500)

</details>

## Returns

Returns an array of resource objects of type [upload\_tag](/docs/content-management-api/resources/upload-tag.md)

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // iterates over every page of results
  for await (const uploadTag of client.uploadTags.listPagedIterator()) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(uploadTag);
  }
}

run();
```

---

# Content Management API — Create a new upload tag

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-tag/create.md

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Pictures of me"`

The tag name

## Returns

Returns a resource object of type [upload\_tag](/docs/content-management-api/resources/upload-tag.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadTag = await client.uploadTags.create({ name: "Pictures of me" });

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadTag);
}

run();
```

Returned output

```javascript
{ id: "42", name: "Pictures of me" }
```

---

# Content Management API — Smart tags

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-smart-tag.md

All the site's upload automatically generated tags

## Object payload

**`id`**

- Type: string
- Example: `"42"`

ID of upload tag

**`type`**

- Type: string

Must be exactly `"upload_smart_tag"`.

**`name`**

- Type: string
- Example: `"building"`

The tag name

---

# Content Management API — List all automatically created upload tags

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-smart-tag/instances.md

The results are sorted by name and paginated by default.

## Query parameters

**`filter`**

- Type: object

Attributes to filter tags

<details>
<summary>Show object format</summary>

**`query`**

- Type: string
- Example: `"foobar"`

Textual query to match.

</details>

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer
- Example: `200`

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 50, maximum is 500)

</details>

## Returns

Returns an array of resource objects of type [upload\_smart\_tag](/docs/content-management-api/resources/upload-smart-tag.md)

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // iterates over every page of results
  for await (const uploadSmartTag of client.uploadSmartTags.listPagedIterator()) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(uploadSmartTag);
  }
}

run();
```

---

# Content Management API — Upload Collection

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-collection.md

In DatoCMS you can organize the uploads present in your administrative area in collection, so that the final editors can easily navigate uploads.

## Object payload

**`id`**

- Type: string
- Example: `"uinr2zfqQLeCo_1O0-ao-Q"`

RFC 4122 UUID of upload collection expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"upload_collection"`.
JSON API type field

**`label`**

- Type: string
- Example: `"Posts"`

The label of the upload collection

**`position`**

- Type: integer
- Example: `1`

Ordering index

**`parent`**

- Type: null, [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md)

Parent upload collection

**`children`**

- Type: Array<[ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md)>

Underlying upload collections

---

# Content Management API — Create a new upload collection

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-collection/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"uinr2zfqQLeCo_1O0-ao-Q"`

RFC 4122 UUID of upload collection expressed in URL-safe base64 format

**`label`**

- Required
- Type: string
- Example: `"Posts"`

The label of the upload collection

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`parent`**

- Optional
- Type: null, [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md)

Parent upload collection

## Returns

Returns a resource object of type [upload\_collection](/docs/content-management-api/resources/upload-collection.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadCollection = await client.uploadCollections.create({
    label: "Posts",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadCollection);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  parent: null,
  children: [{ type: "upload_collection", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Update a upload collection

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-collection/update.md

## Body parameters

**`label`**

- Optional
- Type: string
- Example: `"Posts"`

The label of the upload collection

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`parent`**

- Optional
- Type: null, [ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md)

Parent upload collection

**`children`**

- Optional
- Type: Array<[ResourceLinkage\<"upload_collection"\>](https://www-draft.datocms.com/docs/content-management-api/resources/upload_collection.md)>

Underlying upload collections

## Returns

Returns a resource object of type [upload\_collection](/docs/content-management-api/resources/upload-collection.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadCollectionId = "uinr2zfqQLeCo_1O0-ao-Q";

  const uploadCollection = await client.uploadCollections.update(
    uploadCollectionId,
    { id: "uinr2zfqQLeCo_1O0-ao-Q" },
  );

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadCollection);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  parent: null,
  children: [{ type: "upload_collection", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — List all upload collections

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-collection/instances.md

## Query parameters

**`filter`**

- Type: object

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"42,554"`

IDs to fetch, comma separated

</details>

## Returns

Returns an array of resource objects of type [upload\_collection](/docs/content-management-api/resources/upload-collection.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadCollections = await client.uploadCollections.list();

  for (const uploadCollection of uploadCollections) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(uploadCollection);
  }
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  parent: null,
  children: [{ type: "upload_collection", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Retrieve a upload collection

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-collection/self.md

## Returns

Returns a resource object of type [upload\_collection](/docs/content-management-api/resources/upload-collection.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadCollectionId = "uinr2zfqQLeCo_1O0-ao-Q";

  const uploadCollection =
    await client.uploadCollections.find(uploadCollectionId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadCollection);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  parent: null,
  children: [{ type: "upload_collection", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Delete a upload collection

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-collection/destroy.md

## Returns

Returns a resource object of type [upload\_collection](/docs/content-management-api/resources/upload-collection.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadCollectionId = "uinr2zfqQLeCo_1O0-ao-Q";

  const uploadCollection =
    await client.uploadCollections.destroy(uploadCollectionId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadCollection);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  parent: null,
  children: [{ type: "upload_collection", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Search Index

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index.md

A Search Index is used to index a website to provide DatoCMS Site Search functionality.

## Object payload

**`id`**

- Type: string
- Example: `"1822"`

ID of search_index

**`type`**

- Type: string

Must be exactly `"search_index"`.

**`name`**

- Type: string
- Example: `"Production Website"`

Name of the search index

**`enabled`**

- Type: boolean

Whether the search index is enabled or not

**`frontend_url`**

- Type: string, null
- Example: `"https://www.mywebsite.com/"`

The public URL of the website. This is the starting point from which the website's spidering will start

**`user_agent_suffix`**

- Type: string, null
- Example: `"v1.0.0"`

Optional suffix to append to the DatoCmsSearchBot user agent when indexing the website

**`meta.indexing_status`**

- Type: enum
- Example: `"success"`

Status of the search indexing

<details>
<summary>Show enum values</summary>

**`unstarted`**

**`pending`**

**`success`**

**`failed`**

</details>

**`meta.last_indexing_completed_at`**

- Type: date-time, null
- Example: `"2025-03-30T09:29:14.872Z"`

Timestamp of the last completed indexing

**`build_triggers`**

- Type: Array<[ResourceLinkage\<"build_trigger"\>](https://www-draft.datocms.com/docs/content-management-api/resources/build_trigger.md)>

The build triggers that can trigger this search index

---

# Content Management API — List all search indexes for a site

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index/instances.md

## Returns

Returns an array of resource objects of type [search\_index](/docs/content-management-api/resources/search-index.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndexs = await client.searchIndexes.list();

  for (const searchIndex of searchIndexs) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(searchIndex);
  }
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Production Website",
  enabled: true,
  frontend_url: "https://www.mywebsite.com/",
  user_agent_suffix: "v1.0.0",
  meta: {
    indexing_status: "success",
    last_indexing_completed_at: "2025-03-30T09:29:14.872Z",
  },
  build_triggers: [{ type: "build_trigger", id: "1822" }],
}
```

---

# Content Management API — Retrieve a search index

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index/self.md

## Returns

Returns a resource object of type [search\_index](/docs/content-management-api/resources/search-index.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndexId = "1822";

  const searchIndex = await client.searchIndexes.find(searchIndexId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(searchIndex);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Production Website",
  enabled: true,
  frontend_url: "https://www.mywebsite.com/",
  user_agent_suffix: "v1.0.0",
  meta: {
    indexing_status: "success",
    last_indexing_completed_at: "2025-03-30T09:29:14.872Z",
  },
  build_triggers: [{ type: "build_trigger", id: "1822" }],
}
```

---

# Content Management API — Create a search index

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index/create.md

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Production Website"`

Name of the search index

**`enabled`**

- Required
- Type: boolean

Whether the search index is enabled or not

**`frontend_url`**

- Required
- Type: string, null
- Example: `"https://www.mywebsite.com/"`

The public URL of the website. This is the starting point from which the website's spidering will start

**`user_agent_suffix`**

- Optional
- Type: string, null
- Example: `"v1.0.0"`

Optional suffix to append to the DatoCmsSearchBot user agent when indexing the website

**`build_triggers`**

- Optional
- Type: Array<[ResourceLinkage\<"build_trigger"\>](https://www-draft.datocms.com/docs/content-management-api/resources/build_trigger.md)>

The build triggers that can trigger this search index

## Returns

Returns a resource object of type [search\_index](/docs/content-management-api/resources/search-index.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndex = await client.searchIndexes.create({
    name: "Production Website",
    enabled: true,
    frontend_url: "https://www.mywebsite.com/",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(searchIndex);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Production Website",
  enabled: true,
  frontend_url: "https://www.mywebsite.com/",
  user_agent_suffix: "v1.0.0",
  meta: {
    indexing_status: "success",
    last_indexing_completed_at: "2025-03-30T09:29:14.872Z",
  },
  build_triggers: [{ type: "build_trigger", id: "1822" }],
}
```

---

# Content Management API — Update a search index

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index/update.md

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"Production Website"`

Name of the search index

**`enabled`**

- Optional
- Type: boolean

Whether the search index is enabled or not

**`frontend_url`**

- Optional
- Type: string, null
- Example: `"https://www.mywebsite.com/"`

The public URL of the website. This is the starting point from which the website's spidering will start

**`user_agent_suffix`**

- Optional
- Type: string, null
- Example: `"v1.0.0"`

Optional suffix to append to the DatoCmsSearchBot user agent when indexing the website

**`build_triggers`**

- Optional
- Type: Array<[ResourceLinkage\<"build_trigger"\>](https://www-draft.datocms.com/docs/content-management-api/resources/build_trigger.md)>

The build triggers that can trigger this search index

## Returns

Returns a resource object of type [search\_index](/docs/content-management-api/resources/search-index.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndexId = "1822";

  const searchIndex = await client.searchIndexes.update(searchIndexId, {
    id: "1822",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(searchIndex);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Production Website",
  enabled: true,
  frontend_url: "https://www.mywebsite.com/",
  user_agent_suffix: "v1.0.0",
  meta: {
    indexing_status: "success",
    last_indexing_completed_at: "2025-03-30T09:29:14.872Z",
  },
  build_triggers: [{ type: "build_trigger", id: "1822" }],
}
```

---

# Content Management API — Trigger the indexing process

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index/trigger.md

Manually trigger a spidering of the website to update the search index

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndexId = "1822";
  await client.searchIndexes.trigger(searchIndexId);
}

run();
```

---

# Content Management API — Abort a the current indexing process and mark it as failed

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index/abort.md

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndexId = "1822";
  await client.searchIndexes.abort(searchIndexId);
}

run();
```

---

# Content Management API — Delete a search index

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index/destroy.md

## Returns

Returns a resource object of type [search\_index](/docs/content-management-api/resources/search-index.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndexId = "1822";

  const searchIndex = await client.searchIndexes.destroy(searchIndexId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(searchIndex);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Production Website",
  enabled: true,
  frontend_url: "https://www.mywebsite.com/",
  user_agent_suffix: "v1.0.0",
  meta: {
    indexing_status: "success",
    last_indexing_completed_at: "2025-03-30T09:29:14.872Z",
  },
  build_triggers: [{ type: "build_trigger", id: "1822" }],
}
```

---

# Content Management API — Search result

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-result.md

DatoCMS Site Search is a way to deliver tailored search results to your site visitors. This is the endpoint you can use to query for results.

## Object payload

**`id`**

- Type: string
- Example: `"12adNIIB8rFJF1DoTgCk"`

ID of result

**`type`**

- Type: string

Must be exactly `"search_result"`.

**`title`**

- Type: string
- Example: `"Florence Apartments for Rent | Long Term Student Accommodation Rentals"`

Title of the page

**`body_excerpt`**

- Type: string
- Example: `"Finding a place to live while planning to study abroad in Florence can be both exciting and challenging. With this in mind, Housing in Florence assists you in finding conveniently-located housing based..."`

First 200 characters of page body, unformatted

**`url`**

- Type: string
- Example: `"http://www.website.com/some-page"`

URL

**`score`**

- Type: number
- Example: `11.3`

Search score

**`highlight`**

- Type: object

<details>
<summary>Show object format</summary>

**`title`**

- Type: Array\<string\>, null

**`body`**

- Type: Array\<string\>, null

</details>

---

# Content Management API — Search for results

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-result/instances.md

Returns a list of search results matching your query.

By default, it returns 20 results. You can paginate the results using `limit` and `offset` parameters. In any case, a maximum number of 100 results is returned.

## Query parameters

**`filter`**

- Type: object

Attributes to filter search results

<details>
<summary>Show object format</summary>

**`query`**

- Type: string
- Example: `"florence apartments"`

Text to search

**`fuzzy`**

- Type: boolean

When any value is passed, it enables the fuzzy search: the Levenshtein Edit Distance is used to match more results.

**`search_index_id`**

- Type: string
- Example: `"12345"`

The search index ID on which the search will be performed. If not provided, the first enabled search index will be used.

**`locale`**

- Type: string
- Example: `"it"`

Restrict the search on pages in a specific locale

<details>
<summary>Show deprecated</summary>

**`build_trigger_id`**

- Deprecated
- Type: string
- Example: `"44"`

The build trigger ID or name on which the search will be performed.

Use `search_index_id` instead: this parameter is only supported for backward compatibility and will return an error if the build trigger has multiple search indexes associated.

</details>

</details>

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer
- Example: `200`

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 20, maximum is 100)

</details>

## Returns

Returns an array of resource objects of type [search\_result](/docs/content-management-api/resources/search-result.md)

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // iterates over every page of results
  for await (const searchResult of client.searchResults.listPagedIterator({
    filter: { query: "florence apartments" },
  })) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(searchResult);
  }
}

run();
```

---

# Content Management API — Search indexing activity

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index-event.md

Represents an event occurred during the indexing process via search indexes.

## Object payload

**`id`**

- Type: string
- Example: `"34"`

ID of search index event

**`type`**

- Type: string

Must be exactly `"search_index_event"`.

**`event_type`**

- Type: enum
- Example: `"indexing_success"`

The type of activity

<details>
<summary>Show enum values</summary>

**`indexing_started`**

Site indexing started

**`indexing_success`**

Site indexing completed successfully

**`indexing_failure`**

Site indexing failed

**`indexing_aborted`**

Site indexing aborted by user

</details>

**`data`**

- Type: object

Any details regarding the event

Example:

```json
{
  pages: [
    "https://www.example.com/ (language: en)",
    "https://www.example.com/about (language: en)",
  ],
}
```

**`created_at`**

- Type: date-time
- Example: `"2016-09-20T18:50:24.914Z"`

The moment the activity occurred

**`search_index`**

- Type: [ResourceLinkage\<"search_index"\>](https://www-draft.datocms.com/docs/content-management-api/resources/search_index.md)

Source search index

---

# Content Management API — List all search indexing events

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index-event/instances.md

## Query parameters

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 30, maximum is 500)

</details>

**`filter`**

- Type: object

Attributes to filter

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"42,554"`

IDs to fetch, comma separated

**`fields`**

- Type: object

<details>
<summary>Show object format</summary>

**`search_index_id`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: string

</details>

**`event_type`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: enum
- Example: `"indexing_success"`

The type of activity

<details>
<summary>Show enum values</summary>

**`indexing_started`**

Site indexing started

**`indexing_success`**

Site indexing completed successfully

**`indexing_failure`**

Site indexing failed

**`indexing_aborted`**

Site indexing aborted by user

</details>

</details>

**`created_at`**

- Type: object

<details>
<summary>Show object format</summary>

**`gt`**

- Type: date-time

**`lt`**

- Type: date-time

</details>

</details>

</details>

**`order_by`**

- Type: enum
- Example: `"created_at_desc"`

Fields used to order results

<details>
<summary>Show enum values</summary>

**`search_index_id_asc`**

**`search_index_id_desc`**

**`created_at_asc`**

**`created_at_desc`**

**`event_type_asc`**

**`event_type_desc`**

</details>

## Returns

Returns an array of resource objects of type [search\_index\_event](/docs/content-management-api/resources/search-index-event.md)

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // iterates over every page of results
  for await (const searchIndexEvent of client.searchIndexEvents.listPagedIterator()) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(searchIndexEvent);
  }
}

run();
```

---

# Content Management API — Retrieve a search indexing event

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/search-index-event/self.md

## Returns

Returns a resource object of type [search\_index\_event](/docs/content-management-api/resources/search-index-event.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const searchIndexEventId = "34";

  const searchIndexEvent =
    await client.searchIndexEvents.find(searchIndexEventId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(searchIndexEvent);
}

run();
```

Returned output

```javascript
{
  id: "34",
  event_type: "indexing_success",
  data: {
    pages: [
      "https://www.example.com/ (language: en)",
      "https://www.example.com/about (language: en)",
    ],
  },
  created_at: "2016-09-20T18:50:24.914Z",
  search_index: { type: "search_index", id: "1822" },
}
```

---

# Content Management API — Environment

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/environment.md

[Environments](/docs/general-concepts/primary-and-sandbox-environments.md) make it easier for your development team to **manage and maintain content structure once your content has been published**. You can think of environments like code branches: great for testing, development and pre-production environments.

By default, every project has one environment, called **primary environment**, which is meant to be used for the regular editorial workflow. Additionally, multiple **sandbox environments** can be created by developers to safely test/experiment new changes in the content.

Sandbox environments start out as **exact copies of one of the existing environments** (ie. the primary one). The process of creating a new sandbox starting off from an existing environment is called fork.

Each environment is identified by a name (ie. `master`) and stores the following information:

-   Models
-   Records
-   Uploads
-   Plugins
-   Locales and timezone settings
-   UI Theme (colors and logo)
-   Global SEO settings
-   The content navigation bar

When making changes to any of the aforementioned entities in any environment, including the primary environment, **the data in all other environments isn’t affected** and stays the same.

## Object payload

**`id`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`type`**

- Type: string

Must be exactly `"environment"`.

**`meta.status`**

- Type: enum
- Example: `"ready"`

Status of the environment

<details>
<summary>Show enum values</summary>

**`creating`**

The environment is being forked

**`ready`**

The environment is ready

**`destroying`**

The environment is being destroyed

</details>

**`meta.fork_completion_percentage`**

- Type: number
- Example: `95`

The completion percentage of the fork operation (only present if the status is `creating`)

**`meta.read_only_mode`**

- Type: boolean

Is this environment the in read-only mode because of a fast-fork?

**`meta.created_at`**

- Type: date-time

Date of creation

**`meta.last_data_change_at`**

- Type: date-time

Last data change

**`meta.primary`**

- Type: boolean

Is this environment the primary for the project?

**`meta.forked_from`**

- Type: string, null
- Example: `"main"`

ID of the environment that's been forked to generate this one

---

# Content Management API — Fork an existing environment

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/environment/fork.md

## Query parameters

**`immediate_return`**

- Type: boolean

Whether the call should immediately return a pending environment, or wait for the completion of the fork

**`fast`**

- Type: boolean

Performing a fast fork reduces processing time, but it also prevents writing to the source environment during the process

**`force`**

- Type: boolean

Force the start of fast fork, even if there are collaborators editing some records

## Body parameters

**`id`**

- Required
- Type: string
- Example: `"my-sandbox-env"`

The ID of the forked environment

## Returns

Returns a resource object of type [environment](/docs/content-management-api/resources/environment.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const environmentId = "main";

  const environment = await client.environments.fork(environmentId, {});

  // Check the 'Returned output' tab for the result ☝️
  console.log(environment);
}

run();
```

Returned output

```javascript
{
  id: "main",
  meta: {
    status: "ready",
    created_at: "2020-04-21T07:57:11.124Z",
    read_only_mode: true,
    last_data_change_at: "2020-04-21T07:57:11.124Z",
    primary: true,
    forked_from: "main",
  },
}
```

---

# Content Management API — Promote an environment to primary

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/environment/promote.md

## Returns

Returns a resource object of type [environment](/docs/content-management-api/resources/environment.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const environmentId = "main";

  const environment = await client.environments.promote(environmentId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(environment);
}

run();
```

Returned output

```javascript
{
  id: "main",
  meta: {
    status: "ready",
    created_at: "2020-04-21T07:57:11.124Z",
    read_only_mode: true,
    last_data_change_at: "2020-04-21T07:57:11.124Z",
    primary: true,
    forked_from: "main",
  },
}
```

---

# Content Management API — Rename an environment

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/environment/rename.md

## Body parameters

## Returns

Returns a resource object of type [environment](/docs/content-management-api/resources/environment.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const environmentId = "main";

  const environment = await client.environments.rename(environmentId, {
    id: "renamed-sandbox",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(environment);
}

run();
```

Returned output

```javascript
{
  id: "main",
  meta: {
    status: "ready",
    created_at: "2020-04-21T07:57:11.124Z",
    read_only_mode: true,
    last_data_change_at: "2020-04-21T07:57:11.124Z",
    primary: true,
    forked_from: "main",
  },
}
```

---

# Content Management API — List all environments

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/environment/instances.md

## Returns

Returns an array of resource objects of type [environment](/docs/content-management-api/resources/environment.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const environments = await client.environments.list();

  for (const environment of environments) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(environment);
  }
}

run();
```

Returned output

```javascript
{
  id: "main",
  meta: {
    status: "ready",
    created_at: "2020-04-21T07:57:11.124Z",
    read_only_mode: true,
    last_data_change_at: "2020-04-21T07:57:11.124Z",
    primary: true,
    forked_from: "main",
  },
}
```

---

# Content Management API — Retrieve a environment

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/environment/self.md

## Returns

Returns a resource object of type [environment](/docs/content-management-api/resources/environment.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const environmentId = "main";

  const environment = await client.environments.find(environmentId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(environment);
}

run();
```

Returned output

```javascript
{
  id: "main",
  meta: {
    status: "ready",
    created_at: "2020-04-21T07:57:11.124Z",
    read_only_mode: true,
    last_data_change_at: "2020-04-21T07:57:11.124Z",
    primary: true,
    forked_from: "main",
  },
}
```

---

# Content Management API — Delete a environment

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/environment/destroy.md

## Returns

Returns a resource object of type [environment](/docs/content-management-api/resources/environment.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const environmentId = "main";

  const environment = await client.environments.destroy(environmentId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(environment);
}

run();
```

Returned output

```javascript
{
  id: "main",
  meta: {
    status: "ready",
    created_at: "2020-04-21T07:57:11.124Z",
    read_only_mode: true,
    last_data_change_at: "2020-04-21T07:57:11.124Z",
    primary: true,
    forked_from: "main",
  },
}
```

---

# Content Management API — Maintenance mode

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/maintenance-mode.md

## Object payload

**`id`**

- Type: string
- Example: `"maintenance_mode"`

ID of maintenance_mode

**`type`**

- Type: string

Must be exactly `"maintenance_mode"`.

**`active`**

- Type: boolean

Whether maintenance mode is currently active or not

---

# Content Management API — Retrieve maintenence mode

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/maintenance-mode/self.md

## Returns

Returns a resource object of type [maintenance\_mode](/docs/content-management-api/resources/maintenance-mode.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const maintenanceMode = await client.maintenanceMode.find();

  // Check the 'Returned output' tab for the result ☝️
  console.log(maintenanceMode);
}

run();
```

Returned output

```javascript
{ id: "maintenance_mode", active: false }
```

---

# Content Management API — Activate maintenance mode: this means that the primary environment will be read-only

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/maintenance-mode/activate.md

## Query parameters

**`force`**

- Type: boolean

Force the activation, even if there are collaborators editing some records.

## Returns

Returns a resource object of type [maintenance\_mode](/docs/content-management-api/resources/maintenance-mode.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const maintenanceMode = await client.maintenanceMode.activate();

  // Check the 'Returned output' tab for the result ☝️
  console.log(maintenanceMode);
}

run();
```

Returned output

```javascript
{ id: "maintenance_mode", active: false }
```

---

# Content Management API — De-activate maintenance mode

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/maintenance-mode/deactivate.md

## Returns

Returns a resource object of type [maintenance\_mode](/docs/content-management-api/resources/maintenance-mode.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const maintenanceMode = await client.maintenanceMode.deactivate();

  // Check the 'Returned output' tab for the result ☝️
  console.log(maintenanceMode);
}

run();
```

Returned output

```javascript
{ id: "maintenance_mode", active: false }
```

---

# Content Management API — Menu Item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/menu-item.md

In DatoCMS you can organize the different Models present in your administrative area reordering and grouping them, so that their purpose will be more clear to the final editor.

## Object payload

**`id`**

- Type: string
- Example: `"uinr2zfqQLeCo_1O0-ao-Q"`

RFC 4122 UUID of menu item expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"menu_item"`.

**`label`**

- Type: string
- Example: `"Posts"`

The label of the menu item

**`position`**

- Type: integer
- Example: `1`

Ordering index

**`external_url`**

- Type: null, string

The URL to which the menu item points to

**`open_in_new_tab`**

- Type: boolean

Opens link in new tab (to be used together with `external_url`)

**`item_type`**

- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md), null

Item type associated with the menu item

**`item_type_filter`**

- Type: [ResourceLinkage\<"item_type_filter"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type_filter.md), null

Item type filter associated with the menu item (to be used together with `item_type` relationship)

**`parent`**

- Type: null, [ResourceLinkage\<"menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/menu_item.md)

Parent menu item

**`children`**

- Type: Array<[ResourceLinkage\<"menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/menu_item.md)>

Underlying menu items

---

# Content Management API — Create a new menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/menu-item/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"uinr2zfqQLeCo_1O0-ao-Q"`

RFC 4122 UUID of menu item expressed in URL-safe base64 format

**`label`**

- Required
- Type: string
- Example: `"Posts"`

The label of the menu item

**`external_url`**

- Optional
- Type: null, string

The URL to which the menu item points to

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`open_in_new_tab`**

- Optional
- Type: boolean

Opens link in new tab (to be used together with `external_url`)

**`item_type`**

- Optional
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md), null

Item type associated with the menu item

**`item_type_filter`**

- Optional
- Type: [ResourceLinkage\<"item_type_filter"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type_filter.md), null

Item type filter associated with the menu item (to be used together with `item_type` relationship)

**`parent`**

- Optional
- Type: null, [ResourceLinkage\<"menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/menu_item.md)

Parent menu item

## Returns

Returns a resource object of type [menu\_item](/docs/content-management-api/resources/menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const menuItem = await client.menuItems.create({ label: "Posts" });

  // Check the 'Returned output' tab for the result ☝️
  console.log(menuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  external_url: "",
  open_in_new_tab: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  item_type_filter: { type: "item_type_filter", id: "FF-P5of6Qp-DD2w0xoaa6Q" },
  parent: null,
  children: [{ type: "menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Update a menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/menu-item/update.md

## Body parameters

**`label`**

- Optional
- Type: string
- Example: `"Posts"`

The label of the menu item

**`external_url`**

- Optional
- Type: null, string

The URL to which the menu item points to

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`open_in_new_tab`**

- Optional
- Type: boolean

Opens link in new tab (to be used together with `external_url`)

**`item_type`**

- Optional
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md), null

Item type associated with the menu item

**`item_type_filter`**

- Optional
- Type: [ResourceLinkage\<"item_type_filter"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type_filter.md), null

Item type filter associated with the menu item (to be used together with `item_type` relationship)

**`parent`**

- Optional
- Type: null, [ResourceLinkage\<"menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/menu_item.md)

Parent menu item

## Returns

Returns a resource object of type [menu\_item](/docs/content-management-api/resources/menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const menuItemId = "uinr2zfqQLeCo_1O0-ao-Q";

  const menuItem = await client.menuItems.update(menuItemId, {
    id: "uinr2zfqQLeCo_1O0-ao-Q",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(menuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  external_url: "",
  open_in_new_tab: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  item_type_filter: { type: "item_type_filter", id: "FF-P5of6Qp-DD2w0xoaa6Q" },
  parent: null,
  children: [{ type: "menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — List all menu items

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/menu-item/instances.md

## Query parameters

**`filter`**

- Type: object

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"42,554"`

IDs to fetch, comma separated

</details>

## Returns

Returns an array of resource objects of type [menu\_item](/docs/content-management-api/resources/menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const menuItems = await client.menuItems.list();

  for (const menuItem of menuItems) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(menuItem);
  }
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  external_url: "",
  open_in_new_tab: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  item_type_filter: { type: "item_type_filter", id: "FF-P5of6Qp-DD2w0xoaa6Q" },
  parent: null,
  children: [{ type: "menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Retrieve a menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/menu-item/self.md

## Returns

Returns a resource object of type [menu\_item](/docs/content-management-api/resources/menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const menuItemId = "uinr2zfqQLeCo_1O0-ao-Q";

  const menuItem = await client.menuItems.find(menuItemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(menuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  external_url: "",
  open_in_new_tab: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  item_type_filter: { type: "item_type_filter", id: "FF-P5of6Qp-DD2w0xoaa6Q" },
  parent: null,
  children: [{ type: "menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Delete a menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/menu-item/destroy.md

## Returns

Returns a resource object of type [menu\_item](/docs/content-management-api/resources/menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const menuItemId = "uinr2zfqQLeCo_1O0-ao-Q";

  const menuItem = await client.menuItems.destroy(menuItemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(menuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  external_url: "",
  open_in_new_tab: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  item_type_filter: { type: "item_type_filter", id: "FF-P5of6Qp-DD2w0xoaa6Q" },
  parent: null,
  children: [{ type: "menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Schema Menu Item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/schema-menu-item.md

In DatoCMS you can organize the different models and blocks present in your administrative area reordering and grouping them, so that their purpose will be more clear to the final editor.

## Object payload

**`id`**

- Type: string
- Example: `"uinr2zfqQLeCo_1O0-ao-Q"`

RFC 4122 UUID of schema menu item expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"schema_menu_item"`.
JSON API type field

**`label`**

- Type: null, string
- Example: `"Posts"`

The label of the schema menu item (only present when the schema menu item is not linked to an item type)

**`position`**

- Type: integer
- Example: `1`

Ordering index

**`kind`**

- Type: enum
- Example: `"item_type"`

Indicates if the schema menu item refers to an item type or a modular block

<details>
<summary>Show enum values</summary>

**`item_type`**

**`modular_block`**

</details>

**`item_type`**

- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md), null

Item type associated with the schema menu item

**`parent`**

- Type: null, [ResourceLinkage\<"schema_menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/schema_menu_item.md)

Parent schema menu item

**`children`**

- Type: Array<[ResourceLinkage\<"schema_menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/schema_menu_item.md)>

Underlying schema menu items

---

# Content Management API — Create a new schema menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/schema-menu-item/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"uinr2zfqQLeCo_1O0-ao-Q"`

RFC 4122 UUID of schema menu item expressed in URL-safe base64 format

**`label`**

- Required
- Type: null, string
- Example: `"Posts"`

The label of the schema menu item (only present when the schema menu item is not linked to an item type)

**`kind`**

- Required
- Type: enum
- Example: `"item_type"`

Indicates if the schema menu item refers to an item type or a modular block

<details>
<summary>Show enum values</summary>

**`item_type`**

- Optional

**`modular_block`**

- Optional

</details>

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`item_type`**

- Optional
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md), null

Item type associated with the menu item

**`parent`**

- Optional
- Type: null, [ResourceLinkage\<"schema_menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/schema_menu_item.md)

Parent schema menu item

## Returns

Returns a resource object of type [schema\_menu\_item](/docs/content-management-api/resources/schema-menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaMenuItem = await client.schemaMenuItems.create({
    label: "Posts",
    kind: "item_type",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(schemaMenuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  kind: "item_type",
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  parent: null,
  children: [{ type: "schema_menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Update a schema menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/schema-menu-item/update.md

## Body parameters

**`label`**

- Optional
- Type: null, string
- Example: `"Posts"`

The label of the schema menu item (only present when the schema menu item is not linked to an item type)

**`position`**

- Optional
- Type: integer
- Example: `1`

Ordering index

**`kind`**

- Optional
- Type: enum
- Example: `"item_type"`

Indicates if the schema menu item refers to an item type or a modular block

<details>
<summary>Show enum values</summary>

**`item_type`**

- Optional

**`modular_block`**

- Optional

</details>

**`item_type`**

- Optional
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md), null

Item type associated with the menu item

**`parent`**

- Optional
- Type: null, [ResourceLinkage\<"schema_menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/schema_menu_item.md)

Parent schema menu item

**`children`**

- Optional
- Type: Array<[ResourceLinkage\<"schema_menu_item"\>](https://www-draft.datocms.com/docs/content-management-api/resources/schema_menu_item.md)>

Underlying schema menu items

## Returns

Returns a resource object of type [schema\_menu\_item](/docs/content-management-api/resources/schema-menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaMenuItemId = "uinr2zfqQLeCo_1O0-ao-Q";

  const schemaMenuItem = await client.schemaMenuItems.update(schemaMenuItemId, {
    id: "uinr2zfqQLeCo_1O0-ao-Q",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(schemaMenuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  kind: "item_type",
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  parent: null,
  children: [{ type: "schema_menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — List all schema menu items

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/schema-menu-item/instances.md

## Query parameters

**`filter`**

- Type: object

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"42,554"`

IDs to fetch, comma separated

</details>

## Returns

Returns an array of resource objects of type [schema\_menu\_item](/docs/content-management-api/resources/schema-menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaMenuItems = await client.schemaMenuItems.list();

  for (const schemaMenuItem of schemaMenuItems) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(schemaMenuItem);
  }
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  kind: "item_type",
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  parent: null,
  children: [{ type: "schema_menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Retrieve a schema menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/schema-menu-item/self.md

## Returns

Returns a resource object of type [schema\_menu\_item](/docs/content-management-api/resources/schema-menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaMenuItemId = "uinr2zfqQLeCo_1O0-ao-Q";

  const schemaMenuItem = await client.schemaMenuItems.find(schemaMenuItemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(schemaMenuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  kind: "item_type",
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  parent: null,
  children: [{ type: "schema_menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Delete a schema menu item

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/schema-menu-item/destroy.md

## Returns

Returns a resource object of type [schema\_menu\_item](/docs/content-management-api/resources/schema-menu-item.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const schemaMenuItemId = "uinr2zfqQLeCo_1O0-ao-Q";

  const schemaMenuItem = await client.schemaMenuItems.destroy(schemaMenuItemId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(schemaMenuItem);
}

run();
```

Returned output

```javascript
{
  id: "uinr2zfqQLeCo_1O0-ao-Q",
  label: "Posts",
  position: 1,
  kind: "item_type",
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  parent: null,
  children: [{ type: "schema_menu_item", id: "uinr2zfqQLeCo_1O0-ao-Q" }],
}
```

---

# Content Management API — Uploads filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-filter.md

In DatoCMS you can create filters to help you (and other editors) quickly search for uploads

## Object payload

**`id`**

- Type: string
- Example: `"-Lo34LFSTLmgPToamzJLcg"`

RFC 4122 UUID of upload filter expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"upload_filter"`.

**`name`**

- Type: string
- Example: `"Draft posts"`

The name of the filter

**`filter`**

- Type: object
- Example: `{ status: { eq: "draft" } }`

The actual filter

**`shared`**

- Type: boolean

Whether it's a shared filter or not

---

# Content Management API — Create a new filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-filter/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"-Lo34LFSTLmgPToamzJLcg"`

RFC 4122 UUID of upload filter expressed in URL-safe base64 format

**`name`**

- Required
- Type: string
- Example: `"Draft posts"`

The name of the filter

**`filter`**

- Required
- Type: object
- Example: `{ status: { eq: "draft" } }`

The actual filter

**`shared`**

- Required
- Type: boolean

Whether it's a shared filter or not

## Returns

Returns a resource object of type [upload\_filter](/docs/content-management-api/resources/upload-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadFilter = await client.uploadFilters.create({
    name: "Draft posts",
    filter: { status: { eq: "draft" } },
    shared: true,
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadFilter);
}

run();
```

Returned output

```javascript
{
  id: "-Lo34LFSTLmgPToamzJLcg",
  name: "Draft posts",
  filter: { status: { eq: "draft" } },
  shared: true,
}
```

---

# Content Management API — Update a filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-filter/update.md

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Draft posts"`

The name of the filter

**`filter`**

- Required
- Type: object
- Example: `{ status: { eq: "draft" } }`

The actual filter

**`shared`**

- Optional
- Type: boolean

Whether it's a shared filter or not

## Returns

Returns a resource object of type [upload\_filter](/docs/content-management-api/resources/upload-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadFilterId = "-Lo34LFSTLmgPToamzJLcg";

  const uploadFilter = await client.uploadFilters.update(uploadFilterId, {
    id: "-Lo34LFSTLmgPToamzJLcg",
    name: "Draft posts",
    filter: { status: { eq: "draft" } },
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadFilter);
}

run();
```

Returned output

```javascript
{
  id: "-Lo34LFSTLmgPToamzJLcg",
  name: "Draft posts",
  filter: { status: { eq: "draft" } },
  shared: true,
}
```

---

# Content Management API — List all filters

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-filter/instances.md

## Returns

Returns an array of resource objects of type [upload\_filter](/docs/content-management-api/resources/upload-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadFilters = await client.uploadFilters.list();

  for (const uploadFilter of uploadFilters) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(uploadFilter);
  }
}

run();
```

Returned output

```javascript
{
  id: "-Lo34LFSTLmgPToamzJLcg",
  name: "Draft posts",
  filter: { status: { eq: "draft" } },
  shared: true,
}
```

---

# Content Management API — Retrieve a filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-filter/self.md

## Returns

Returns a resource object of type [upload\_filter](/docs/content-management-api/resources/upload-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadFilterId = "-Lo34LFSTLmgPToamzJLcg";

  const uploadFilter = await client.uploadFilters.find(uploadFilterId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadFilter);
}

run();
```

Returned output

```javascript
{
  id: "-Lo34LFSTLmgPToamzJLcg",
  name: "Draft posts",
  filter: { status: { eq: "draft" } },
  shared: true,
}
```

---

# Content Management API — Delete a filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/upload-filter/destroy.md

## Returns

Returns a resource object of type [upload\_filter](/docs/content-management-api/resources/upload-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const uploadFilterId = "-Lo34LFSTLmgPToamzJLcg";

  const uploadFilter = await client.uploadFilters.destroy(uploadFilterId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(uploadFilter);
}

run();
```

Returned output

```javascript
{
  id: "-Lo34LFSTLmgPToamzJLcg",
  name: "Draft posts",
  filter: { status: { eq: "draft" } },
  shared: true,
}
```

---

# Content Management API — Model filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type-filter.md

In DatoCMS you can create filters to help you (and other editors) quickly search for records

## Object payload

**`id`**

- Type: string
- Example: `"FF-P5of6Qp-DD2w0xoaa6Q"`

RFC 4122 UUID of filter expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"item_type_filter"`.

**`name`**

- Type: string
- Example: `"Draft posts"`

The name of the filter

**`filter`**

- Type: object

The actual filter. It follows the form of the `filter` query parameter of the [List all records](https://www.datocms.com/docs/content-management-api/resources/item/instances) endpoint.

Example:

```json
{
  query: "foo bar",
  fields: {
    _status: { eq: "draft" },
    title: {
      matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
    },
  },
}
```

**`columns`**

- Type: Array\<object\>, null

The columns to show with this filter

Example:

```json
[
  { name: "_preview", width: 0.6 },
  { name: "slug", width: 0.1 },
  { name: "_status", width: 0.1 },
  { name: "_updated_at", width: 0.2 },
]
```

<details>
<summary>Show objects format inside array</summary>

**`name`**

- Type: string

Can be either the API key of a model's field, or one of the following meta columns: `id`, `_preview`, `_updated_at`, `_created_at`, `_creator`, `_status`, `_published_at`, `_first_published_at`, `_publication_scheduled_at`, `_unpublishing_scheduled_at`, `position` (only for sortable models), `_stage (only for models associated with a workflow).

**`width`**

- Type: number

The percentage width for the column (float, from 0 to 1.0)

</details>

**`order_by`**

- Type: string, null
- Example: `"_updated_at_ASC"`

The ordering to apply with this filter, or `null` for the default model ordering. It follows the form of the `order_by` query parameter of the [List all records](https://www.datocms.com/docs/content-management-api/resources/item/instances) endpoint.

**`shared`**

- Type: boolean

Whether it's a shared filter or not

**`item_type`**

- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

Model associated with the filter

---

# Content Management API — Create a new filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type-filter/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"FF-P5of6Qp-DD2w0xoaa6Q"`

RFC 4122 UUID of filter expressed in URL-safe base64 format

**`name`**

- Required
- Type: string
- Example: `"Draft posts"`

The name of the filter

**`filter`**

- Optional
- Type: object

The actual filter. It follows the form of the `filter` query parameter of the [List all records](https://www.datocms.com/docs/content-management-api/resources/item/instances) endpoint.

Example:

```json
{
  query: "foo bar",
  fields: {
    _status: { eq: "draft" },
    title: {
      matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
    },
  },
}
```

**`columns`**

- Optional
- Type: Array\<object\>, null

The columns to show with this filter

Example:

```json
[
  { name: "_preview", width: 0.6 },
  { name: "slug", width: 0.1 },
  { name: "_status", width: 0.1 },
  { name: "_updated_at", width: 0.2 },
]
```

<details>
<summary>Show objects format inside array</summary>

**`name`**

- Required
- Type: string

Can be either the API key of a model's field, or one of the following meta columns: `id`, `_preview`, `_updated_at`, `_created_at`, `_creator`, `_status`, `_published_at`, `_first_published_at`, `_publication_scheduled_at`, `_unpublishing_scheduled_at`, `position` (only for sortable models), `_stage (only for models associated with a workflow).

**`width`**

- Required
- Type: number

The percentage width for the column (float, from 0 to 1.0)

</details>

**`order_by`**

- Optional
- Type: string, null
- Example: `"_updated_at_ASC"`

The ordering to apply with this filter, or `null` for the default model ordering. It follows the form of the `order_by` query parameter of the [List all records](https://www.datocms.com/docs/content-management-api/resources/item/instances) endpoint.

**`shared`**

- Optional
- Type: boolean

Whether it's a shared filter or not

**`item_type`**

- Required
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

Model associated with the filter

## Returns

Returns a resource object of type [item\_type\_filter](/docs/content-management-api/resources/item-type-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemTypeFilter = await client.itemTypeFilters.create({
    name: "Draft posts",
    item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemTypeFilter);
}

run();
```

Returned output

```javascript
{
  id: "FF-P5of6Qp-DD2w0xoaa6Q",
  name: "Draft posts",
  filter: {
    query: "foo bar",
    fields: {
      _status: { eq: "draft" },
      title: {
        matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
      },
    },
  },
  columns: [
    { name: "_preview", width: 0.6 },
    { name: "slug", width: 0.1 },
    { name: "_status", width: 0.1 },
    { name: "_updated_at", width: 0.2 },
  ],
  order_by: "_updated_at_ASC",
  shared: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Update a filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type-filter/update.md

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"Draft posts"`

The name of the filter

**`columns`**

- Optional
- Type: Array\<object\>, null

The columns to show with this filter

Example:

```json
[
  { name: "_preview", width: 0.6 },
  { name: "slug", width: 0.1 },
  { name: "_status", width: 0.1 },
  { name: "_updated_at", width: 0.2 },
]
```

<details>
<summary>Show objects format inside array</summary>

**`name`**

- Required
- Type: string

Can be either the API key of a model's field, or one of the following meta columns: `id`, `_preview`, `_updated_at`, `_created_at`, `_creator`, `_status`, `_published_at`, `_first_published_at`, `_publication_scheduled_at`, `_unpublishing_scheduled_at`, `position` (only for sortable models), `_stage (only for models associated with a workflow).

**`width`**

- Required
- Type: number

The percentage width for the column (float, from 0 to 1.0)

</details>

**`order_by`**

- Optional
- Type: string, null
- Example: `"_updated_at_ASC"`

The ordering to apply with this filter, or `null` for the default model ordering. It follows the form of the `order_by` query parameter of the [List all records](https://www.datocms.com/docs/content-management-api/resources/item/instances) endpoint.

**`shared`**

- Optional
- Type: boolean

Whether it's a shared filter or not

**`filter`**

- Optional
- Type: object

The actual filter. It follows the form of the `filter` query parameter of the [List all records](https://www.datocms.com/docs/content-management-api/resources/item/instances) endpoint.

Example:

```json
{
  query: "foo bar",
  fields: {
    _status: { eq: "draft" },
    title: {
      matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
    },
  },
}
```

**`item_type`**

- Optional
- Type: [ResourceLinkage\<"item_type"\>](https://www-draft.datocms.com/docs/content-management-api/resources/item_type.md)

Model associated with the filter

## Returns

Returns a resource object of type [item\_type\_filter](/docs/content-management-api/resources/item-type-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemTypeFilterId = "FF-P5of6Qp-DD2w0xoaa6Q";

  const itemTypeFilter = await client.itemTypeFilters.update(itemTypeFilterId, {
    id: "FF-P5of6Qp-DD2w0xoaa6Q",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemTypeFilter);
}

run();
```

Returned output

```javascript
{
  id: "FF-P5of6Qp-DD2w0xoaa6Q",
  name: "Draft posts",
  filter: {
    query: "foo bar",
    fields: {
      _status: { eq: "draft" },
      title: {
        matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
      },
    },
  },
  columns: [
    { name: "_preview", width: 0.6 },
    { name: "slug", width: 0.1 },
    { name: "_status", width: 0.1 },
    { name: "_updated_at", width: 0.2 },
  ],
  order_by: "_updated_at_ASC",
  shared: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — List all filters

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type-filter/instances.md

## Returns

Returns an array of resource objects of type [item\_type\_filter](/docs/content-management-api/resources/item-type-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemTypeFilters = await client.itemTypeFilters.list();

  for (const itemTypeFilter of itemTypeFilters) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(itemTypeFilter);
  }
}

run();
```

Returned output

```javascript
{
  id: "FF-P5of6Qp-DD2w0xoaa6Q",
  name: "Draft posts",
  filter: {
    query: "foo bar",
    fields: {
      _status: { eq: "draft" },
      title: {
        matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
      },
    },
  },
  columns: [
    { name: "_preview", width: 0.6 },
    { name: "slug", width: 0.1 },
    { name: "_status", width: 0.1 },
    { name: "_updated_at", width: 0.2 },
  ],
  order_by: "_updated_at_ASC",
  shared: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Retrieve a filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type-filter/self.md

## Returns

Returns a resource object of type [item\_type\_filter](/docs/content-management-api/resources/item-type-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemTypeFilterId = "FF-P5of6Qp-DD2w0xoaa6Q";

  const itemTypeFilter = await client.itemTypeFilters.find(itemTypeFilterId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemTypeFilter);
}

run();
```

Returned output

```javascript
{
  id: "FF-P5of6Qp-DD2w0xoaa6Q",
  name: "Draft posts",
  filter: {
    query: "foo bar",
    fields: {
      _status: { eq: "draft" },
      title: {
        matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
      },
    },
  },
  columns: [
    { name: "_preview", width: 0.6 },
    { name: "slug", width: 0.1 },
    { name: "_status", width: 0.1 },
    { name: "_updated_at", width: 0.2 },
  ],
  order_by: "_updated_at_ASC",
  shared: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Delete a filter

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/item-type-filter/destroy.md

## Returns

Returns a resource object of type [item\_type\_filter](/docs/content-management-api/resources/item-type-filter.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const itemTypeFilterId = "FF-P5of6Qp-DD2w0xoaa6Q";

  const itemTypeFilter = await client.itemTypeFilters.destroy(itemTypeFilterId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(itemTypeFilter);
}

run();
```

Returned output

```javascript
{
  id: "FF-P5of6Qp-DD2w0xoaa6Q",
  name: "Draft posts",
  filter: {
    query: "foo bar",
    fields: {
      _status: { eq: "draft" },
      title: {
        matches: { pattern: "qux", case_sensitive: "false", regexp: "false" },
      },
    },
  },
  columns: [
    { name: "_preview", width: 0.6 },
    { name: "slug", width: 0.1 },
    { name: "_status", width: 0.1 },
    { name: "_updated_at", width: 0.2 },
  ],
  order_by: "_updated_at_ASC",
  shared: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
}
```

---

# Content Management API — Plugin

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/plugin.md

Plugins enable developers to replace DatoCMS field components with HTML5 applications so the editing experiences of the DatoCMS web app can be customized.

## Object payload

**`id`**

- Type: string
- Example: `"RMAMRffBRlmBuDlQsIWZ0g"`

RFC 4122 UUID of plugin expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"plugin"`.

**`name`**

- Type: string
- Example: `"5 stars"`

The name of the plugin

**`description`**

- Type: null, string
- Example: `"A better rating experience!"`

A description of the plugin

**`url`**

- Type: string
- Example: `"https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.ts"`

The entry point URL of the plugin

**`parameters`**

- Type: null, object
- Example: `{ devMode: true }`

Global plugin configuration. Plugins can persist whatever information they want in this object to reuse it later. Refer to the CMA for details about technical limits. It returns `null` if the credentials you are using cannot edit the schema of the project.

**`package_name`**

- Type: null, string
- Example: `"datocms-plugin-star-rating-editor"`

NPM package name of the plugin (or null if it's a private plugin)

**`package_version`**

- Type: null, string
- Example: `"0.0.4"`

The installed version of the plugin (or null if it's a private plugin)

**`permissions`**

- Type: Array\<string\>

Permissions granted to this plugin

**`enabled`**

- Type: boolean

Whether the plugin is enabled or not. When disabled, the plugin behaves as if it didn't exist: fields using it as editor/addon fall back to the default editor, and any sidebar panel/page/config screen/asset source it registers does not render.

**`meta.version`**

- Type: string
- Example: `"2"`

Version of the plugin. Legacy plugins are v1, new plugins are v2

<details>
<summary>Show deprecated</summary>

**`plugin_type`**

- Deprecated
- Type: null, enum

The type of field extension a legacy plugin implements

This field makes sense for legacy plugins only. Modern plugins declare their capabilities at run-time.

<details>
<summary>Show enum values</summary>

**`field_editor`**

Field editor plugin

**`sidebar`**

Sidebar plugin

**`field_addon`**

Field addon plugin

</details>

**`field_types`**

- Deprecated
- Type: null, Array\<string\>

On which types of field in which a legacy plugin can be used

This field makes sense for legacy plugins only. Modern plugins declare their capabilities at run-time.

**`parameter_definitions`**

- Deprecated
- Type: null, object

The schema for the parameters a legacy plugin can persist

This field makes sense for legacy plugins only. Modern plugins declare can store anything they want in the parameters attribute.

<details>
<summary>Show object format</summary>

**`global`**

- Type: Array

**`instance`**

- Type: Array

</details>

</details>

---

# Content Management API — Create a new plugin

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/plugin/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"RMAMRffBRlmBuDlQsIWZ0g"`

RFC 4122 UUID of plugin expressed in URL-safe base64 format

**`package_name`**

- Optional
- Type: null, string
- Example: `"datocms-plugin-star-rating-editor"`

NPM package name of the public plugin you want to install. For public plugins, that's the only attribute you need to pass.

**`name`**

- Optional
- Type: string
- Example: `"5 stars"`

The name of the plugin. Only to be passed if package name key is not specified.

**`description`**

- Optional
- Type: null, string
- Example: `"A better rating experience!"`

A description of the plugin. Only to be passed if package name key is not specified.

**`url`**

- Optional
- Type: string
- Example: `"https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.ts"`

The entry point URL of the plugin. Only to be passed if package name key is not specified.

**`permissions`**

- Optional
- Type: Array\<string\>

Permissions granted to this plugin. Only to be passed if package name key is not specified.

**`enabled`**

- Optional
- Type: boolean

Whether the plugin is enabled or not. Defaults to `true`.

<details>
<summary>Show deprecated</summary>

**`plugin_type`**

- Deprecated
- Type: enum
- Example: `"field_editor"`

The type of field extension this legacy plugin implements. Only to be passed if package name key is not specified.

Pass this field only if you plan to create a legacy plugin. Modern plugins declare their capabilities at run-time.

<details>
<summary>Show enum values</summary>

**`field_editor`**

- Optional

Field editor plugin

**`sidebar`**

- Optional

Sidebar plugin

**`field_addon`**

- Optional

Field addon plugin

</details>

**`field_types`**

- Deprecated
- Type: Array\<string\>
- Example: `["integer", "float"]`

On which types of field in which this legacy plugin can be used. Only to be passed if package name key is not specified.

Pass this field only if you plan to create a legacy plugin. Modern plugins declare their capabilities at run-time.

**`parameter_definitions`**

- Deprecated
- Type: object

The schema for the parameters this legacy plugin can persist

This field makes sense for legacy plugins only. Modern plugins declare can store anything they want in the parameters attribute.

Example:

```json
{
  global: [
    { id: "devMode", type: "boolean", label: "Run in development mode" },
  ],
  instance: [
    {
      id: "halfStars",
      type: "boolean",
      label: "Allow half stars ratings?",
      default: false,
      hint: "If enabled, rate using whole stars, if enabled, it doesn't use half-steps",
    },
    {
      id: "totalStars",
      type: "integer",
      label: "Amount of stars to show",
      default: 5,
      hint: "",
    },
  ],
}
```

<details>
<summary>Show object format</summary>

**`global`**

- Required
- Type: Array

**`instance`**

- Required
- Type: Array

</details>

</details>

## Returns

Returns a resource object of type [plugin](/docs/content-management-api/resources/plugin.md)

## Other examples

###### Example Installation of a public plugin from NPM

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const plugin = await client.plugins.create({
    package_name: "datocms-plugin-star-rating-editor",
  });

  console.log(plugin);
}

run();
```

Returned output

```javascript
const result = {
  type: "plugin",
  id: "124",
  name: "5 stars",
  description: "A better rating experience!",
  package_name: "datocms-plugin-star-rating-editor",
  package_version: "0.0.4",
  url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.js",
  permissions: ["currentUserAccessToken"],
  parameters: {},
};
```


###### Example Creation of a private plugin

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const plugin = await client.plugins.create({
    name: "5 stars",
    description: "A better rating experience!",
    url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.js",
    permissions: ["currentUserAccessToken"],
  });

  console.log(plugin);
}

run();
```

Returned output

```javascript
const result = {
  type: "plugin",
  id: "124",
  name: "5 stars",
  description: "A better rating experience!",
  url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.js",
  permissions: ["currentUserAccessToken"],
  parameters: {},
};
```

---

# Content Management API — Update a plugin

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/plugin/update.md

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"5 stars"`

The name of the plugin

**`description`**

- Optional
- Type: null, string
- Example: `"A better rating experience!"`

A description of the plugin

**`url`**

- Optional
- Type: string
- Example: `"https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.ts"`

The entry point URL of the plugin

**`package_name`**

- Optional
- Type: string
- Example: `"datocms-plugin-star-rating-editor"`

Turns a private plugin into a Marketplace plugin, installing the specified package from the DatoCMS Marketplace. Only accepted for private, non-legacy plugins, and cannot be combined with other attributes (except `enabled`). Name, description, URL, version and permissions are replaced with the ones published in the Marketplace, while the existing `parameters` are preserved.

**`parameters`**

- Optional
- Type: object
- Example: `{ devMode: true }`

Global plugin configuration. Plugins can persist whatever information they want in this object to reuse it later. Refer to the CMA for details about technical limits.

**`package_version`**

- Optional
- Type: null, string
- Example: `"0.0.4"`

The installed version of the plugin (or null if it's a private plugin)

**`permissions`**

- Optional
- Type: Array\<string\>

Permissions granted to this plugin

**`enabled`**

- Optional
- Type: boolean

Whether the plugin is enabled or not. When disabled, the plugin behaves as if it didn't exist: fields using it as editor/addon fall back to the default editor, and any sidebar panel/page/config screen/asset source it registers does not render.

## Returns

Returns a resource object of type [plugin](/docs/content-management-api/resources/plugin.md)

## Other examples

###### Example Update of plugin global parameters (both private and public)

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const pluginId = "124";

  const plugin = await client.plugins.update(pluginId, {
    parameters: { foo: "bar" },
  });

  console.log(plugin);
}

run();
```

Returned output

```javascript
const result = {
  type: "plugin",
  id: "124",
  name: "5 stars",
  /* ... */
  parameters: { foo: "bar" },
};
```


###### Example Upgrade of a public plugin

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const pluginId = "124";

  const plugin = await client.plugins.update(pluginId, {
    package_version: "2.0.0",
  });

  console.log(plugin);
}

run();
```

Returned output

```javascript
const result = {
  type: "plugin",
  id: "124",
  name: "5 stars",
  /* ... */
  package_version: "2.0.0",
};
```


###### Example Update of private plugin configuration

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const pluginId = "124";

  const plugin = await client.plugins.update(pluginId, {
    name: "5 stars",
    description: "A better rating experience!",
    url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.js",
    permissions: ["currentUserAccessToken"],
  });

  console.log(plugin);
}

run();
```

Returned output

```javascript
const result = {
  type: "plugin",
  id: "124",
  name: "5 stars",
  description: "A better rating experience!",
  package_name: null,
  package_version: null,
  url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.js",
  permissions: ["currentUserAccessToken"],
  parameters: { foo: "bar" },
};
```


###### Example Turning a private plugin into a Marketplace plugin

Once a privately-hosted plugin gets published to the DatoCMS Marketplace, you can turn the private plugin into a Marketplace plugin. Its global parameters are preserved.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const pluginId = "124";

  const plugin = await client.plugins.update(pluginId, {
    package_name: "datocms-plugin-star-rating-editor",
  });

  console.log(plugin);
}

run();
```

Returned output

```javascript
const result = {
  type: "plugin",
  id: "124",
  name: "Star rating",
  description: "A better rating experience!",
  package_name: "datocms-plugin-star-rating-editor",
  package_version: "0.0.4",
  url: "https://plugins-cdn.datocms.com/datocms-plugin-star-rating-editor@0.0.4/dist/index.html",
  permissions: [],
  parameters: { foo: "bar" },
  enabled: true,
};
```

---

# Content Management API — List all plugins

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/plugin/instances.md

## Returns

Returns an array of resource objects of type [plugin](/docs/content-management-api/resources/plugin.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const plugins = await client.plugins.list();

  for (const plugin of plugins) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(plugin);
  }
}

run();
```

Returned output

```javascript
{
  id: "RMAMRffBRlmBuDlQsIWZ0g",
  name: "5 stars",
  description: "A better rating experience!",
  url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.ts",
  parameters: { devMode: true },
  package_name: "datocms-plugin-star-rating-editor",
  package_version: "0.0.4",
  permissions: ["currentUserAccessToken"],
  enabled: true,
  meta: { version: "2" },
}
```

---

# Content Management API — Retrieve a plugin

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/plugin/self.md

## Returns

Returns a resource object of type [plugin](/docs/content-management-api/resources/plugin.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const pluginId = "RMAMRffBRlmBuDlQsIWZ0g";

  const plugin = await client.plugins.find(pluginId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(plugin);
}

run();
```

Returned output

```javascript
{
  id: "RMAMRffBRlmBuDlQsIWZ0g",
  name: "5 stars",
  description: "A better rating experience!",
  url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.ts",
  parameters: { devMode: true },
  package_name: "datocms-plugin-star-rating-editor",
  package_version: "0.0.4",
  permissions: ["currentUserAccessToken"],
  enabled: true,
  meta: { version: "2" },
}
```

---

# Content Management API — Delete a plugin

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/plugin/destroy.md

## Returns

Returns a resource object of type [plugin](/docs/content-management-api/resources/plugin.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const pluginId = "RMAMRffBRlmBuDlQsIWZ0g";

  const plugin = await client.plugins.destroy(pluginId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(plugin);
}

run();
```

Returned output

```javascript
{
  id: "RMAMRffBRlmBuDlQsIWZ0g",
  name: "5 stars",
  description: "A better rating experience!",
  url: "https://cdn.rawgit.com/datocms/extensions/master/samples/five-stars/extension.ts",
  parameters: { devMode: true },
  package_name: "datocms-plugin-star-rating-editor",
  package_version: "0.0.4",
  permissions: ["currentUserAccessToken"],
  enabled: true,
  meta: { version: "2" },
}
```

---

# Content Management API — Retrieve all fields using the plugin

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/plugin/fields.md

## Returns

Returns an array of resource objects of type [field](/docs/content-management-api/resources/field.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const pluginId = "RMAMRffBRlmBuDlQsIWZ0g";

  const plugins = await client.plugins.fields(pluginId);

  for (const plugin of plugins) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(plugin);
  }
}

run();
```

Returned output

```javascript
{
  id: "Pkg-oztERp6o-Rj76nYKJg",
  label: "Title",
  field_type: "string",
  api_key: "title",
  localized: true,
  validators: { required: {} },
  position: 1,
  hint: "This field will be used as post title",
  default_value: { en: "A default value", it: "Un valore di default" },
  appearance: {
    editor: "single_line",
    parameters: { heading: false },
    addons: [{ id: "1234", field_extension: "lorem_ipsum", parameters: {} }],
  },
  deep_filtering_enabled: true,
  content_link_enabled: true,
  item_type: { type: "item_type", id: "DxMaW10UQiCmZcuuA-IkkA" },
  fieldset: null,
}
```

---

# Content Management API — Workflow

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/workflow.md

Through workflows it is possible to set up a precise state machine able to bring a draft content up to the final publication (and beyond), through a series of intermediate, fully customizable approval steps.

## Object payload

**`id`**

- Type: string
- Example: `"uJzC2b6YQg-DW2A5edpQYQ"`

RFC 4122 UUID of workflow expressed in URL-safe base64 format

**`type`**

- Type: string

Must be exactly `"workflow"`.

**`name`**

- Type: string
- Example: `"Approval by editors required"`

The name of the workflow

**`stages`**

- Type: Array\<object\>
- Example: `[{ id: "waiting_for_review", name: "Waiting for review", initial: true }]`

The stages of the workflow

<details>
<summary>Show objects format inside array</summary>

**`id`**

- Type: string
- Example: `"waiting_for_review"`

ID of the stage

**`name`**

- Type: string
- Example: `"Waiting for review"`

Name of the stage

**`description`**

- Type: string, null
- Example: `"Editor has finished writing and is waiting for approval from a supervisor"`

Description of the stage

**`initial`**

- Type: boolean

Whether this is the initial stage or not

</details>

**`api_key`**

- Type: string
- Example: `"approval_by_editors"`

Workflow API key

---

# Content Management API — Create a new workflow

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/workflow/create.md

## Body parameters

**`id`**

- Optional
- Type: string
- Example: `"uJzC2b6YQg-DW2A5edpQYQ"`

RFC 4122 UUID of workflow expressed in URL-safe base64 format

**`name`**

- Required
- Type: string
- Example: `"Approval by editors required"`

The name of the workflow

**`stages`**

- Required
- Type: Array\<object\>
- Example: `[{ id: "waiting_for_review", name: "Waiting for review", initial: true }]`

The stages of the workflow

<details>
<summary>Show objects format inside array</summary>

**`id`**

- Required
- Type: string
- Example: `"waiting_for_review"`

ID of the stage

**`name`**

- Required
- Type: string
- Example: `"Waiting for review"`

Name of the stage

**`description`**

- Optional
- Type: string, null
- Example: `"Editor has finished writing and is waiting for approval from a supervisor"`

Description of the stage

**`initial`**

- Optional
- Type: boolean

Whether this is the initial stage or not

</details>

**`api_key`**

- Required
- Type: string
- Example: `"approval_by_editors"`

Workflow API key

## Returns

Returns a resource object of type [workflow](/docs/content-management-api/resources/workflow.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const workflow = await client.workflows.create({
    name: "Approval by editors required",
    stages: [
      { id: "waiting_for_review", name: "Waiting for review", initial: true },
    ],
    api_key: "approval_by_editors",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(workflow);
}

run();
```

Returned output

```javascript
{
  id: "uJzC2b6YQg-DW2A5edpQYQ",
  name: "Approval by editors required",
  stages: [
    { id: "waiting_for_review", name: "Waiting for review", initial: true },
  ],
  api_key: "approval_by_editors",
}
```

---

# Content Management API — Update a workflow

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/workflow/update.md

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"Approval by editors required"`

The name of the workflow

**`api_key`**

- Optional
- Type: string
- Example: `"approval_by_editors"`

Workflow API key

**`stages`**

- Optional
- Type: Array\<object\>
- Example: `[{ id: "waiting_for_review", name: "Waiting for review", initial: true }]`

The stages of the workflow

<details>
<summary>Show objects format inside array</summary>

**`id`**

- Required
- Type: string
- Example: `"waiting_for_review"`

ID of the stage

**`name`**

- Required
- Type: string
- Example: `"Waiting for review"`

Name of the stage

**`description`**

- Optional
- Type: string, null
- Example: `"Editor has finished writing and is waiting for approval from a supervisor"`

Description of the stage

**`initial`**

- Optional
- Type: boolean

Whether this is the initial stage or not

</details>

## Returns

Returns a resource object of type [workflow](/docs/content-management-api/resources/workflow.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const workflowId = "uJzC2b6YQg-DW2A5edpQYQ";

  const workflow = await client.workflows.update(workflowId, {
    id: "uJzC2b6YQg-DW2A5edpQYQ",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(workflow);
}

run();
```

Returned output

```javascript
{
  id: "uJzC2b6YQg-DW2A5edpQYQ",
  name: "Approval by editors required",
  stages: [
    { id: "waiting_for_review", name: "Waiting for review", initial: true },
  ],
  api_key: "approval_by_editors",
}
```

---

# Content Management API — List all workflows

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/workflow/instances.md

## Returns

Returns an array of resource objects of type [workflow](/docs/content-management-api/resources/workflow.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const workflows = await client.workflows.list();

  for (const workflow of workflows) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(workflow);
  }
}

run();
```

Returned output

```javascript
{
  id: "uJzC2b6YQg-DW2A5edpQYQ",
  name: "Approval by editors required",
  stages: [
    { id: "waiting_for_review", name: "Waiting for review", initial: true },
  ],
  api_key: "approval_by_editors",
}
```

---

# Content Management API — Retrieve a workflow

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/workflow/self.md

## Returns

Returns a resource object of type [workflow](/docs/content-management-api/resources/workflow.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const workflowId = "uJzC2b6YQg-DW2A5edpQYQ";

  const workflow = await client.workflows.find(workflowId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(workflow);
}

run();
```

Returned output

```javascript
{
  id: "uJzC2b6YQg-DW2A5edpQYQ",
  name: "Approval by editors required",
  stages: [
    { id: "waiting_for_review", name: "Waiting for review", initial: true },
  ],
  api_key: "approval_by_editors",
}
```

---

# Content Management API — Delete a workflow

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/workflow/destroy.md

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const workflowId = "uJzC2b6YQg-DW2A5edpQYQ";
  await client.workflows.destroy(workflowId);
}

run();
```

---

# Content Management API — Asynchronous job

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/job.md

## Object payload

**`id`**

- Type: string
- Example: `"4235"`

ID of job

**`type`**

- Type: string

Must be exactly `"job"`.

---

# Content Management API — Job result

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/job-result.md

Some API endpoint give results asynchronously, returning the ID of a job.

## Object payload

**`id`**

- Type: string
- Example: `"34"`

ID of job result

**`type`**

- Type: string

Must be exactly `"job_result"`.

**`status`**

- Type: integer
- Example: `200`

Status of delayed HTTP response

**`payload`**

- Type: null, object
- Example: `{ data: { id: 999, type: "item_type", attributes: { some: "attributes" } } }`

JSON API response of the HTTP request

---

# Content Management API — Retrieve a job result

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/job-result/self.md

## Returns

Returns a resource object of type [job\_result](/docs/content-management-api/resources/job-result.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const jobResultId = "34";

  const jobResult = await client.jobResults.find(jobResultId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(jobResult);
}

run();
```

Returned output

```javascript
{
  id: "34",
  status: 200,
  payload: {
    data: { id: 999, type: "item_type", attributes: { some: "attributes" } },
  },
}
```

---

# Content Management API — Account

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/account.md

DatoCMS account

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of account

**`type`**

- Type: string

Must be exactly `"account"`.

**`email`**

- Type: string
- Example: `"foo@bar.com"`

Email

**`first_name`**

- Type: string, null
- Example: `"Mark"`

First name

**`last_name`**

- Type: string, null
- Example: `"Smith"`

Last name

**`company`**

- Type: string, null
- Example: `"Dundler Mifflin"`

Company name

---

# Content Management API — Organization

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/organization.md

DatoCMS organization

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of organization

**`type`**

- Type: string

Must be exactly `"organization"`.

**`name`**

- Type: string
- Example: `"Acme Inc."`

Name of the organization

---

# Content Management API — Invitation

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site-invitation.md

A DatoCMS administrative area can be accessed by multiple people. Every invitation is linked to a specific Role, which describes what actions it will be able to perform once the user will register.

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of invitation

**`type`**

- Type: string

Must be exactly `"site_invitation"`.

**`email`**

- Type: string
- Example: `"mark.smith@example.com"`

Email

**`expired`**

- Type: boolean
- Example: `"mark.smith@example.com"`

Whether this invitation has expired

**`invitation_link`**

- Type: null, string
- Example: `"https://dashboard.datocms.com/join-site?email=my-email%40datocms.comff&id=43796&preference=signup&token=xxx"`

The link to join a DatoCMS project. Shown only on creation and reset

**`role`**

- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Role

---

# Content Management API — Invite a new user

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site-invitation/create.md

## Body parameters

**`email`**

- Required
- Type: string
- Example: `"mark.smith@example.com"`

Email

**`role`**

- Required
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Role

## Returns

Returns a resource object of type [site\_invitation](/docs/content-management-api/resources/site-invitation.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const siteInvitation = await client.siteInvitations.create({
    email: "mark.smith@example.com",
    role: { type: "role", id: "34" },
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(siteInvitation);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  expired: "mark.smith@example.com",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Update an invitation

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site-invitation/update.md

## Body parameters

**`role`**

- Optional
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Role

## Returns

Returns a resource object of type [site\_invitation](/docs/content-management-api/resources/site-invitation.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const siteInvitationId = "312";

  const siteInvitation = await client.siteInvitations.update(siteInvitationId, {
    id: "312",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(siteInvitation);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  expired: "mark.smith@example.com",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — List all invitations

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site-invitation/instances.md

## Returns

Returns an array of resource objects of type [site\_invitation](/docs/content-management-api/resources/site-invitation.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const siteInvitations = await client.siteInvitations.list();

  for (const siteInvitation of siteInvitations) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(siteInvitation);
  }
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  expired: "mark.smith@example.com",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Retrieve an invitation

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site-invitation/self.md

## Returns

Returns a resource object of type [site\_invitation](/docs/content-management-api/resources/site-invitation.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const siteInvitationId = "312";

  const siteInvitation = await client.siteInvitations.find(siteInvitationId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(siteInvitation);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  expired: "mark.smith@example.com",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Delete an invitation

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site-invitation/destroy.md

## Returns

Returns a resource object of type [site\_invitation](/docs/content-management-api/resources/site-invitation.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const siteInvitationId = "312";

  const siteInvitation = await client.siteInvitations.destroy(siteInvitationId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(siteInvitation);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  expired: "mark.smith@example.com",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Resend an invitation

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/site-invitation/resend.md

Resends the email invitation

## Returns

Returns a resource object of type [site\_invitation](/docs/content-management-api/resources/site-invitation.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const siteInvitationId = "312";

  const siteInvitation = await client.siteInvitations.resend(siteInvitationId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(siteInvitation);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  expired: "mark.smith@example.com",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Collaborator

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/user.md

A DatoCMS administrative area can be accessed by multiple people. Every collaborator is linked to a specific Role, which describes what actions it will be able to perform once logged in.

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of collaborator

**`type`**

- Type: string

Must be exactly `"user"`.

**`email`**

- Type: string
- Example: `"mark.smith@example.com"`

Email

**`is_2fa_active`**

- Type: boolean, null

Whether 2-factor authentication is active for this account or not. It returns `null` if the credentials you are using cannot manage the collaborators of the project.

**`full_name`**

- Type: string
- Example: `"Mark Smith"`

Full name

**`is_active`**

- Type: boolean

Whether the user is active or not

**`meta.last_access`**

- Type: date-time, null
- Example: `"2018-03-25T21:50:24.914Z"`

Date of last reading/interaction. It returns `null` if the credentials you are using cannot manage the collaborators of the project.

**`role`**

- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Role

---

# Content Management API — Update a collaborator

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/user/update.md

## Body parameters

**`is_active`**

- Optional
- Type: boolean

Whether the user is active or not

**`role`**

- Optional
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Role

## Returns

Returns a resource object of type [user](/docs/content-management-api/resources/user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const userId = "312";

  const user = await client.users.update(userId, { id: "312" });

  // Check the 'Returned output' tab for the result ☝️
  console.log(user);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  is_2fa_active: true,
  full_name: "Mark Smith",
  is_active: true,
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — List all collaborators

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/user/instances.md

## Returns

Returns an array of resource objects of type [user](/docs/content-management-api/resources/user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const users = await client.users.list();

  for (const user of users) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(user);
  }
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  is_2fa_active: true,
  full_name: "Mark Smith",
  is_active: true,
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Retrieve a collaborator

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/user/self.md

## Query parameters

**`include`**

- Type: string
- Example: `"role"`

Comma-separated list of [relationship paths](https://jsonapi.org/format/#fetching-includes). A relationship path is a dot-separated list of relationship names. Allowed relationship paths: `role`.

## Returns

Returns a resource object of type [user](/docs/content-management-api/resources/user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const userId = "312";

  const user = await client.users.find(userId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(user);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  is_2fa_active: true,
  full_name: "Mark Smith",
  is_active: true,
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Retrieve current signed-in user

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/user/me.md

## Query parameters

**`include`**

- Type: string
- Example: `"role"`

Comma-separated list of [relationship paths](https://jsonapi.org/format/#fetching-includes). A relationship path is a dot-separated list of relationship names. Allowed relationship paths: `role`.

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const user = await client.users.findMe();

  // Check the 'Returned output' tab for the result ☝️
  console.log(user);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  is_2fa_active: true,
  full_name: "Mark Smith",
  is_active: true,
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Delete a collaborator

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/user/destroy.md

## Query parameters

**`destination_user_type`**

- Type: enum
- Example: `"user"`

New owner for resources previously owned by the deleted user. This argument specifies the new owner type.

<details>
<summary>Show enum values</summary>

**`account`**

**`user`**

**`access_token`**

**`sso_user`**

</details>

**`destination_user_id`**

- Type: string
- Example: `"7865"`

New owner for resources previously owned by the deleted user. This argument specifies the new owner ID.

## Returns

Returns a resource object of type [user](/docs/content-management-api/resources/user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const userId = "312";

  const user = await client.users.destroy(userId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(user);
}

run();
```

Returned output

```javascript
{
  id: "312",
  email: "mark.smith@example.com",
  is_2fa_active: true,
  full_name: "Mark Smith",
  is_active: true,
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Role

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/role.md

A Role groups the permissions that govern what a credential can do in a project. The same role definition is applied to **collaborators**, **SSO users**, and **API tokens** alike — design roles around what the *credential* should be allowed to do, not who is holding it.

> [!PROTIP] 📘 Same role, different identities
> Ask "what is the *credential* allowed to do?" — not "what is this *person* allowed to do?". For API tokens specifically, the role's permissions are further constrained by the token's API surface flags (`can_access_cda`, `can_access_cda_preview`, `can_access_cma`); see the [API token](/docs/content-management-api/resources/access-token.md) resource for details.

## How permissions are computed

Most of the granular permissions on a role come as a `positive_<resource>_permissions` / `negative_<resource>_permissions` pair: build triggers, search indexes, records (`item_type`), uploads. They all follow the same rule:

> Effective permissions = `(inherited ∪ positive_*) − negative_*`

Positive entries (and entries pulled in via `relationships.inherits_permissions_from`) grant access. Negative entries always win when they overlap. The idiomatic recipe for "almost everything" is a single `action: "all"` positive entry plus targeted negative entries to subtract — instead of enumerating each allowed action.

> [!WARNING] ⚠️ Send positive_* and negative_* together
> For each resource family (records, uploads, build triggers, search indexes), the matching `positive_*` and `negative_*` arrays must be **both present or both absent** in a create/update payload. On **update**, sent arrays *replace* the stored ones wholesale, so always read the role first and pass back the existing entries on the side you're not changing — sending `[]` to satisfy the constraint will erase everything that was there. (On create, `[]` is fine since there's nothing to lose.) The [Update endpoint](/docs/content-management-api/resources/role/update.md) documents an SDK helper that handles this diff for records and uploads.

The computed result is exposed on every role response under `meta.final_permissions`; the raw declared values stay on `attributes.*`. See [Effective vs declared permissions](/docs/content-management-api/resources/role.md#effective-vs-declared-permissions) below.

## Project-level permissions

These attributes gate access to project-wide capabilities. They apply uniformly across the whole project; granular control over individual records and uploads lives under [Per-environment content permissions](/docs/content-management-api/resources/role.md#per-environment-content-permissions).

-   **Project-wide flags.** Boolean attributes named `can_*` (`can_edit_schema`, `can_manage_environments`, `can_manage_access_tokens`, …) cover the schema, environments, users, webhooks, and so on — see the property table for the full list.
-   **Environment access.** `environments_access` controls *which* environments the credential can enter at all (`all`, `primary_only`, `sandbox_only`, or `none`). Use `none` when the role is meant only to be inherited from.
-   **Build triggers.** The role may **manually fire** the build triggers listed in `positive_build_trigger_permissions`, minus those listed in `negative_build_trigger_permissions`. Use `build_trigger: null` on an entry to cover every trigger at once. Creating, editing, or deleting trigger definitions is gated separately by `can_manage_build_triggers`.
-   **Search indexes.** The role may **manually re-index** the search indexes listed in `positive_search_index_permissions`, minus those listed in `negative_search_index_permissions`. Use `search_index: null` on an entry to cover every index. Managing the index definitions themselves is gated separately by `can_manage_search_indexes`.

## Per-environment content permissions

The role's access to **records** and **uploads** is governed by two positive/negative array pairs. Every entry is **scoped to a single environment** via the required `environment` field — to grant the same permission across multiple environments, repeat the entry once per environment id (or use `inherits_permissions_from` together with `environments_access`). The computation is the same `(inherited ∪ positive_*) − negative_*` rule from [How permissions are computed](/docs/content-management-api/resources/role.md#how-permissions-are-computed), evaluated per environment.

###### Records

Permission entries live in `positive_item_type_permissions` (and the `negative_*` counterpart). Each entry is a discriminated union keyed by `action`:

-   `all` — every action below
-   `read` — read records
-   `create` — create new records
-   `update` — edit existing records
-   `publish` — publish/unpublish records
-   `duplicate` — duplicate records
-   `delete` — destroy records
-   `edit_creator` — change a record's `creator` relationship
-   `take_over` — wrest a record from another user currently editing it
-   `move_to_stage` — move a record between workflow stages

Per entry you can also restrict by:

-   `item_type` — restrict to a specific model (`null` = all models)
-   `workflow` — restrict to records associated with a workflow (mutually exclusive with `item_type`)
-   `on_creator` — `anyone`, `self` (records the credential created), or `role` (records created by anyone with this role)
-   `localization_scope` + `locale` — for `create`/`update`/`publish`/`all`: restrict to localized vs non-localized content, optionally pinning to one locale (on `all` the scope is forced to `"all"`)
-   `on_stage` / `to_stage` — for workflow-aware actions: restrict to records currently on a stage, or to moves towards a stage

The shape of each entry depends on the `action` — see the property tables on each endpoint for which sub-fields are valid per branch.

> [!WARNING] ⚠️ Some restrictors require an Enterprise plan
> Workflow-aware permissions — the `move_to_stage` action and the `workflow` / `on_stage` / `to_stage` restrictors — require [Workflows](https://www.datocms.com/features/workflows.md), an Enterprise feature. Per-content-scope restrictions are also gated: only `localization_scope: "all"` is available on every plan, while `"localized"` (with its companion `locale`) and `"not_localized"` both require Enterprise. Setting any of these on a non-Enterprise project will return an error — check the [pricing page](https://www.datocms.com/pricing.md) before relying on them.

###### Uploads

Permission entries live in `positive_upload_permissions` (and the `negative_*` counterpart). Same discriminated-union shape as records, with the upload-relevant actions (`read`, `create`, `update`, `delete`, `edit_creator`, `replace_asset`, `move`, `all`), scoped by `upload_collection` instead of `item_type`. The `move` action also accepts `move_to_upload_collection` to restrict the destination of the move.

## Inheriting from other roles

`relationships.inherits_permissions_from` accepts a list of role ids whose permissions are unioned into this role's positive set before the negative set is subtracted (per [How permissions are computed](/docs/content-management-api/resources/role.md#how-permissions-are-computed)). This is how built-in roles are typically extended without copying their full permission tree — duplicate the closest built-in role, then add a `negative_*` entry to take something away, or set `inherits_permissions_from` and add only the positive entries that differ.

## Effective vs declared permissions

Two views of a role's permissions are surfaced on the response:

-   **`attributes.*`** — the permissions declared *on this role directly*. This is what was sent on create/update; it does not reflect anything inherited from `relationships.inherits_permissions_from`.
-   **`meta.final_permissions`** — the **effective** permissions after walking the inheritance chain and applying the rule from [How permissions are computed](/docs/content-management-api/resources/role.md#how-permissions-are-computed). This is the set actually enforced when a credential bound to this role makes a request.

When debugging "why can't this user do X?", read `meta.final_permissions`, not `attributes`.

## Object payload

**`id`**

- Type: string
- Example: `"34"`

ID of role

**`type`**

- Type: string

Must be exactly `"role"`.

**`name`**

- Type: string
- Example: `"Editor"`

The name of the role

**`can_edit_site`**

- Type: boolean

Can change project-wide settings (project name, internal subdomain, frontend preview URL, deployment settings)

**`can_edit_favicon`**

- Type: boolean

Can edit favicon, global SEO settings and no-index policy

**`can_edit_schema`**

- Type: boolean

Can create and edit the project schema: models, block models, fields, fieldsets, validators, and plugins

**`can_manage_menu`**

- Type: boolean

Can customize content navigation bar

**`can_manage_users`**

- Type: boolean

Can create and edit roles and invite/remove collaborators

**`can_manage_shared_filters`**

- Type: boolean

Can create and edit shared filters (both for models and the media area)

**`can_manage_search_indexes`**

- Type: boolean

Can create and edit search indexes

**`can_manage_upload_collections`**

- Type: boolean

Can create and edit upload collections

**`can_manage_environments`**

- Type: boolean

Can create, fork, and delete sandbox environments. Promotion to primary is gated separately by `can_promote_environments`.

**`can_manage_webhooks`**

- Type: boolean

Can create and edit webhooks

**`environments_access`**

- Type: enum
- Example: `"primary_only"`

Specifies the environments the user can access

<details>
<summary>Show enum values</summary>

**`all`**

Grants access to all environments

**`primary_only`**

Grants access exclusively to the primary environment

**`sandbox_only`**

Grants access exclusively to sandbox environments

**`none`**

No access to any environment. This value is typically used when the role is intended to inherit access settings from other roles

</details>

**`can_manage_sso`**

- Type: boolean

Can manage Single Sign-On settings

**`can_access_audit_log`**

- Type: boolean

Can access Audit Log

**`can_manage_workflows`**

- Type: boolean

Can create and edit workflows

**`can_edit_environment`**

- Type: boolean

Can edit per-environment settings of the environments this role has access to: locales, timezone, and UI theme. This is *not* about creating or switching environments — see `can_manage_environments` for that, and `environments_access` for which environments this role can enter at all.

**`can_promote_environments`**

- Type: boolean

Can promote a sandbox environment to primary (atomic swap) and toggle the project's maintenance mode. Distinct from `can_manage_environments`, which covers creating/forking/deleting sandboxes.

**`can_manage_build_triggers`**

- Type: boolean

Can create and edit build triggers

**`can_manage_access_tokens`**

- Type: boolean

Can manage API tokens

**`can_perform_site_search`**

- Type: boolean

Can perform Site Search API calls

**`can_access_build_events_log`**

- Type: boolean

Can access the build events log

**`can_access_search_index_events_log`**

- Type: boolean

Can access the search index events log

**`positive_item_type_permissions`**

- Type: Array\<object\>

Allowed actions on a model (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). Idiomatic recipes:
- To grant every action, use a single `action: "all"` entry with `localization_scope: "all"`.
- To grant a subset (e.g. create+read+update but not delete), prefer a single `action: "all"` entry plus `negative_item_type_permissions` entries for the actions to exclude — instead of listing each allowed action separately.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

**`publish`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

**`edit_creator`**

**`take_over`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`negative_item_type_permissions`**

- Type: Array\<object\>

Prohibited actions on a model (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions (e.g. forbid `delete`).

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

**`publish`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

**`edit_creator`**

**`take_over`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`positive_upload_permissions`**

- Type: Array\<object\>

Allowed actions on uploads (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). To grant a subset, prefer a single `action: "all"` entry plus `negative_upload_permissions` entries for the actions to exclude.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

**`delete`**

**`edit_creator`**

**`replace_asset`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`negative_upload_permissions`**

- Type: Array\<object\>

Prohibited actions on uploads (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

**`delete`**

**`edit_creator`**

**`replace_asset`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`positive_build_trigger_permissions`**

- Type: Array\<object\>

Build triggers this role is allowed to **manually fire**. An entry with `build_trigger: null` covers every build trigger. Note: this does not control creating/editing build triggers themselves — that is gated by `can_manage_build_triggers`.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Type: string, null

</details>

**`negative_build_trigger_permissions`**

- Type: Array\<object\>

Build triggers this role is **forbidden** from manually firing. Negative entries take precedence over positive ones; pair with a `build_trigger: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Type: string, null

</details>

**`positive_search_index_permissions`**

- Type: Array\<object\>

Search indexes this role is allowed to **manually re-index**. An entry with `search_index: null` covers every search index. Note: this does not control creating/editing search indexes themselves — that is gated by `can_manage_search_indexes`.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Type: string, null

</details>

**`negative_search_index_permissions`**

- Type: Array\<object\>

Search indexes this role is **forbidden** from manually re-indexing. Negative entries take precedence over positive ones; pair with a `search_index: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Type: string, null

</details>

**`meta.final_permissions`**

- Type: object

The final set of permissions considering also inherited roles

<details>
<summary>Show object format</summary>

**`can_edit_site`**

- Type: boolean

Can change project-wide settings (project name, internal subdomain, frontend preview URL, deployment settings)

**`can_edit_favicon`**

- Type: boolean

Can edit favicon, global SEO settings and no-index policy

**`can_edit_schema`**

- Type: boolean

Can create and edit the project schema: models, block models, fields, fieldsets, validators, and plugins

**`can_manage_menu`**

- Type: boolean

Can customize content navigation bar

**`can_manage_users`**

- Type: boolean

Can create and edit roles and invite/remove collaborators

**`can_manage_environments`**

- Type: boolean

Can create, fork, and delete sandbox environments. Promotion to primary is gated separately by `can_promote_environments`.

**`can_manage_webhooks`**

- Type: boolean

Can create and edit webhooks

**`environments_access`**

- Type: enum
- Example: `"primary_only"`

Specifies the environments the user can access

<details>
<summary>Show enum values</summary>

**`all`**

Grants access to all environments

**`primary_only`**

Grants access exclusively to the primary environment

**`sandbox_only`**

Grants access exclusively to sandbox environments

**`none`**

No access to any environment. This value is typically used when the role is intended to inherit access settings from other roles

</details>

**`can_manage_sso`**

- Type: boolean

Can manage Single Sign-On settings

**`can_access_audit_log`**

- Type: boolean

Can access Audit Log

**`can_manage_workflows`**

- Type: boolean

Can create and edit workflows

**`can_edit_environment`**

- Type: boolean

Can edit per-environment settings of the environments this role has access to: locales, timezone, and UI theme. This is *not* about creating or switching environments — see `can_manage_environments` for that, and `environments_access` for which environments this role can enter at all.

**`can_promote_environments`**

- Type: boolean

Can promote a sandbox environment to primary (atomic swap) and toggle the project's maintenance mode. Distinct from `can_manage_environments`, which covers creating/forking/deleting sandboxes.

**`can_manage_shared_filters`**

- Type: boolean

Can create and edit shared filters (both for models and the media area)

**`can_manage_search_indexes`**

- Type: boolean

Can create and edit search indexes

**`can_manage_build_triggers`**

- Type: boolean

Can create and edit build triggers

**`can_manage_upload_collections`**

- Type: boolean

Can create and edit upload collections

**`can_manage_access_tokens`**

- Type: boolean

Can manage API tokens

**`can_perform_site_search`**

- Type: boolean

Can perform Site Search API calls

**`can_access_build_events_log`**

- Type: boolean

Can access the build events log

**`can_access_search_index_events_log`**

- Type: boolean

Can access the search index events log

**`positive_item_type_permissions`**

- Type: Array\<object\>

Allowed actions on a model (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). Idiomatic recipes:
- To grant every action, use a single `action: "all"` entry with `localization_scope: "all"`.
- To grant a subset (e.g. create+read+update but not delete), prefer a single `action: "all"` entry plus `negative_item_type_permissions` entries for the actions to exclude — instead of listing each allowed action separately.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

**`publish`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

**`edit_creator`**

**`take_over`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`negative_item_type_permissions`**

- Type: Array\<object\>

Prohibited actions on a model (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions (e.g. forbid `delete`).

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

**`publish`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Content under a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

**`edit_creator`**

**`take_over`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`item_type`**

- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`positive_upload_permissions`**

- Type: Array\<object\>

Allowed actions on uploads (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). To grant a subset, prefer a single `action: "all"` entry plus `negative_upload_permissions` entries for the actions to exclude.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

**`delete`**

**`edit_creator`**

**`replace_asset`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`negative_upload_permissions`**

- Type: Array\<object\>

Prohibited actions on uploads (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`localization_scope`**

- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

Any content (localized/unlocalized)

**`localized`**

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

Non-localized content

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

**`delete`**

**`edit_creator`**

**`replace_asset`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

</details>

**`environment`**

- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

Created by anyone

**`self`**

Created by the user itself

**`role`**

Created by a user with the same role

</details>

**`upload_collection`**

- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`positive_build_trigger_permissions`**

- Type: Array\<object\>

Build triggers this role is allowed to **manually fire**. An entry with `build_trigger: null` covers every build trigger. Note: this does not control creating/editing build triggers themselves — that is gated by `can_manage_build_triggers`.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Type: string, null

</details>

**`negative_build_trigger_permissions`**

- Type: Array\<object\>

Build triggers this role is **forbidden** from manually firing. Negative entries take precedence over positive ones; pair with a `build_trigger: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Type: string, null

</details>

**`positive_search_index_permissions`**

- Type: Array\<object\>

Search indexes this role is allowed to **manually re-index**. An entry with `search_index: null` covers every search index. Note: this does not control creating/editing search indexes themselves — that is gated by `can_manage_search_indexes`.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Type: string, null

</details>

**`negative_search_index_permissions`**

- Type: Array\<object\>

Search indexes this role is **forbidden** from manually re-indexing. Negative entries take precedence over positive ones; pair with a `search_index: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Type: string, null

</details>

</details>

**`inherits_permissions_from`**

- Type: Array<[ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)>

The roles from which this role inherits permissions

---

# Content Management API — Create a new role

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/role/create.md

Creates a new role in the project. The role is immediately assignable to **collaborators**, **SSO users**, and **API tokens**.

For the conceptual model — project-level vs content permissions, the discriminated-union shape of each `positive_*` / `negative_*` entry, and how inheritance is resolved — see the [Role resource overview](/docs/content-management-api/resources/role.md).

> [!PROTIP] 💡 Don't start from scratch
> Most custom roles are easier to build by [duplicating](/docs/content-management-api/resources/role/duplicate.md) the closest-matching built-in role (e.g. *Editor*) and then editing the result, rather than constructing a permission tree from zero. Use this endpoint when you genuinely need a role that doesn't resemble any of the existing ones.

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Editor"`

The name of the role

**`can_edit_favicon`**

- Optional
- Type: boolean

Can edit favicon, global SEO settings and no-index policy

**`can_edit_site`**

- Optional
- Type: boolean

Can change project-wide settings (project name, internal subdomain, frontend preview URL, deployment settings)

**`can_edit_schema`**

- Optional
- Type: boolean

Can create and edit the project schema: models, block models, fields, fieldsets, validators, and plugins

**`can_manage_menu`**

- Optional
- Type: boolean

Can customize content navigation bar

**`can_edit_environment`**

- Optional
- Type: boolean

Can edit per-environment settings of the environments this role has access to: locales, timezone, and UI theme. This is *not* about creating or switching environments — see `can_manage_environments` for that, and `environments_access` for which environments this role can enter at all.

**`can_promote_environments`**

- Optional
- Type: boolean

Can promote a sandbox environment to primary (atomic swap) and toggle the project's maintenance mode. Distinct from `can_manage_environments`, which covers creating/forking/deleting sandboxes.

**`environments_access`**

- Optional
- Type: enum
- Example: `"primary_only"`

Specifies the environments the user can access

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Grants access to all environments

**`primary_only`**

- Optional

Grants access exclusively to the primary environment

**`sandbox_only`**

- Optional

Grants access exclusively to sandbox environments

**`none`**

- Optional

No access to any environment. This value is typically used when the role is intended to inherit access settings from other roles

</details>

**`can_manage_users`**

- Optional
- Type: boolean

Can create and edit roles and invite/remove collaborators

**`can_manage_shared_filters`**

- Optional
- Type: boolean

Can create and edit shared filters (both for models and the media area)

**`can_manage_search_indexes`**

- Optional
- Type: boolean

Can create and edit search indexes

**`can_manage_upload_collections`**

- Optional
- Type: boolean

Can create and edit upload collections

**`can_manage_build_triggers`**

- Optional
- Type: boolean

Can create and edit build triggers

**`can_manage_webhooks`**

- Optional
- Type: boolean

Can create and edit webhooks

**`can_manage_environments`**

- Optional
- Type: boolean

Can create, fork, and delete sandbox environments. Promotion to primary is gated separately by `can_promote_environments`.

**`can_manage_sso`**

- Optional
- Type: boolean

Can manage Single Sign-On settings

**`can_access_audit_log`**

- Optional
- Type: boolean

Can access Audit Log

**`can_manage_workflows`**

- Optional
- Type: boolean

Can create and edit workflows

**`can_manage_access_tokens`**

- Optional
- Type: boolean

Can manage API tokens

**`can_perform_site_search`**

- Optional
- Type: boolean

Can perform Site Search API calls

**`can_access_build_events_log`**

- Optional
- Type: boolean

Can access the build events log

**`can_access_search_index_events_log`**

- Optional
- Type: boolean

Can access the search index events log

**`positive_item_type_permissions`**

- Optional
- Type: Array\<object\>

Allowed actions on a model (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). Idiomatic recipes:
- To grant every action, use a single `action: "all"` entry with `localization_scope: "all"`.
- To grant a subset (e.g. create+read+update but not delete), prefer a single `action: "all"` entry plus `negative_item_type_permissions` entries for the actions to exclude — instead of listing each allowed action separately.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`negative_item_type_permissions`**

- Optional
- Type: Array\<object\>

Prohibited actions on a model (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions (e.g. forbid `delete`).

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`positive_upload_permissions`**

- Optional
- Type: Array\<object\>

Allowed actions on uploads (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). To grant a subset, prefer a single `action: "all"` entry plus `negative_upload_permissions` entries for the actions to exclude.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`negative_upload_permissions`**

- Optional
- Type: Array\<object\>

Prohibited actions on uploads (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`positive_build_trigger_permissions`**

- Optional
- Type: Array\<object\>

Build triggers this role is allowed to **manually fire**. An entry with `build_trigger: null` covers every build trigger. Note: this does not control creating/editing build triggers themselves — that is gated by `can_manage_build_triggers`.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`negative_build_trigger_permissions`**

- Optional
- Type: Array\<object\>

Build triggers this role is **forbidden** from manually firing. Negative entries take precedence over positive ones; pair with a `build_trigger: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`positive_search_index_permissions`**

- Optional
- Type: Array\<object\>

Search indexes this role is allowed to **manually re-index**. An entry with `search_index: null` covers every search index. Note: this does not control creating/editing search indexes themselves — that is gated by `can_manage_search_indexes`.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

**`negative_search_index_permissions`**

- Optional
- Type: Array\<object\>

Search indexes this role is **forbidden** from manually re-indexing. Negative entries take precedence over positive ones; pair with a `search_index: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

**`meta.final_permissions`**

- Required
- Type: object

The final set of permissions considering also inherited roles

<details>
<summary>Show object format</summary>

**`can_edit_site`**

- Required
- Type: boolean

Can change project-wide settings (project name, internal subdomain, frontend preview URL, deployment settings)

**`can_edit_favicon`**

- Required
- Type: boolean

Can edit favicon, global SEO settings and no-index policy

**`can_edit_schema`**

- Required
- Type: boolean

Can create and edit the project schema: models, block models, fields, fieldsets, validators, and plugins

**`can_manage_menu`**

- Required
- Type: boolean

Can customize content navigation bar

**`can_manage_users`**

- Required
- Type: boolean

Can create and edit roles and invite/remove collaborators

**`can_manage_environments`**

- Required
- Type: boolean

Can create, fork, and delete sandbox environments. Promotion to primary is gated separately by `can_promote_environments`.

**`can_manage_webhooks`**

- Required
- Type: boolean

Can create and edit webhooks

**`environments_access`**

- Required
- Type: enum
- Example: `"primary_only"`

Specifies the environments the user can access

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Grants access to all environments

**`primary_only`**

- Optional

Grants access exclusively to the primary environment

**`sandbox_only`**

- Optional

Grants access exclusively to sandbox environments

**`none`**

- Optional

No access to any environment. This value is typically used when the role is intended to inherit access settings from other roles

</details>

**`can_manage_sso`**

- Required
- Type: boolean

Can manage Single Sign-On settings

**`can_access_audit_log`**

- Required
- Type: boolean

Can access Audit Log

**`can_manage_workflows`**

- Required
- Type: boolean

Can create and edit workflows

**`can_edit_environment`**

- Required
- Type: boolean

Can edit per-environment settings of the environments this role has access to: locales, timezone, and UI theme. This is *not* about creating or switching environments — see `can_manage_environments` for that, and `environments_access` for which environments this role can enter at all.

**`can_promote_environments`**

- Required
- Type: boolean

Can promote a sandbox environment to primary (atomic swap) and toggle the project's maintenance mode. Distinct from `can_manage_environments`, which covers creating/forking/deleting sandboxes.

**`can_manage_shared_filters`**

- Required
- Type: boolean

Can create and edit shared filters (both for models and the media area)

**`can_manage_search_indexes`**

- Required
- Type: boolean

Can create and edit search indexes

**`can_manage_build_triggers`**

- Required
- Type: boolean

Can create and edit build triggers

**`can_manage_upload_collections`**

- Required
- Type: boolean

Can create and edit upload collections

**`can_manage_access_tokens`**

- Required
- Type: boolean

Can manage API tokens

**`can_perform_site_search`**

- Required
- Type: boolean

Can perform Site Search API calls

**`can_access_build_events_log`**

- Required
- Type: boolean

Can access the build events log

**`can_access_search_index_events_log`**

- Required
- Type: boolean

Can access the search index events log

**`positive_item_type_permissions`**

- Required
- Type: Array\<object\>

Allowed actions on a model (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). Idiomatic recipes:
- To grant every action, use a single `action: "all"` entry with `localization_scope: "all"`.
- To grant a subset (e.g. create+read+update but not delete), prefer a single `action: "all"` entry plus `negative_item_type_permissions` entries for the actions to exclude — instead of listing each allowed action separately.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`negative_item_type_permissions`**

- Required
- Type: Array\<object\>

Prohibited actions on a model (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions (e.g. forbid `delete`).

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`positive_upload_permissions`**

- Required
- Type: Array\<object\>

Allowed actions on uploads (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). To grant a subset, prefer a single `action: "all"` entry plus `negative_upload_permissions` entries for the actions to exclude.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`negative_upload_permissions`**

- Required
- Type: Array\<object\>

Prohibited actions on uploads (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`positive_build_trigger_permissions`**

- Required
- Type: Array\<object\>

Build triggers this role is allowed to **manually fire**. An entry with `build_trigger: null` covers every build trigger. Note: this does not control creating/editing build triggers themselves — that is gated by `can_manage_build_triggers`.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`negative_build_trigger_permissions`**

- Required
- Type: Array\<object\>

Build triggers this role is **forbidden** from manually firing. Negative entries take precedence over positive ones; pair with a `build_trigger: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`positive_search_index_permissions`**

- Required
- Type: Array\<object\>

Search indexes this role is allowed to **manually re-index**. An entry with `search_index: null` covers every search index. Note: this does not control creating/editing search indexes themselves — that is gated by `can_manage_search_indexes`.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

**`negative_search_index_permissions`**

- Required
- Type: Array\<object\>

Search indexes this role is **forbidden** from manually re-indexing. Negative entries take precedence over positive ones; pair with a `search_index: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

</details>

**`inherits_permissions_from`**

- Optional
- Type: Array<[ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)>

The roles from which this role inherits permissions

## Returns

Returns a resource object of type [role](/docs/content-management-api/resources/role.md)

## Other examples

###### Example Inherit from a built-in role and subtract one action

A role that inherits the project's built-in *Editor* role but **forbids `delete`** on records. The new role's `attributes.*` is almost empty — every grant comes from the inherited role; only the negative entry is declared directly. Reading `meta.final_permissions` on the response shows the resolved set.

This is the canonical "extend a built-in role" recipe: do not copy the parent's permission tree, just point at it via `inherits_permissions_from` and use `negative_item_type_permissions` to subtract.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Look up the built-in "Editor" role to inherit from.
  const allRoles = await client.roles.list();
  const editorRole = allRoles.find((role) => role.name === "Editor")!;

  // Find the primary environment id — required on every permission entry.
  const environments = await client.environments.list();
  const primaryEnv = environments.find(
    (environment) => environment.meta.primary,
  )!;

  // Create a new role that inherits everything from "Editor" except the
  // `delete` action on records, which is subtracted via a negative entry.
  // The API requires positive_* and negative_* arrays to be both present or
  // both absent — so we pass an explicit empty positive array.
  const role = await client.roles.create({
    name: "Editor (no delete)",
    inherits_permissions_from: [{ type: "role", id: editorRole.id }],
    positive_item_type_permissions: [],
    negative_item_type_permissions: [
      {
        environment: primaryEnv.id,
        action: "delete",
        on_creator: "anyone",
      },
    ],
  });

  console.log("Created role:", role.id, "—", role.name);
  console.log(
    "Effective record permissions (final_permissions):",
    JSON.stringify(
      role.meta.final_permissions.positive_item_type_permissions,
      null,
      2,
    ),
  );
  console.log(
    "Effective negative entries:",
    JSON.stringify(
      role.meta.final_permissions.negative_item_type_permissions,
      null,
      2,
    ),
  );
}

run();
```

Returned output

```javascript
Created role: 443077 — Editor (no delete)
Effective record permissions (final_permissions): [
  {
    "environment": "main",
    "item_type": null,
    "workflow": null,
    "on_stage": null,
    "to_stage": null,
    "action": "all",
    "on_creator": "anyone",
    "localization_scope": "all",
    "locale": null
  }
]
Effective negative entries: [
  {
    "environment": "main",
    "item_type": null,
    "workflow": null,
    "on_stage": null,
    "to_stage": null,
    "action": "delete",
    "on_creator": "anyone",
    "localization_scope": null,
    "locale": null
  }
]
```

---

# Content Management API — Update a role

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/role/update.md

Updates an existing role. Any attribute or relationship omitted from the payload is left unchanged.

The full `positive_*` / `negative_*` permission arrays are **replaced wholesale** when sent — there is no "patch a single permission entry" operation on this endpoint. Read the role first and forward the entries you don't want to change, or use the SDK helper described below.

## Safer permission edits with `updateCurrentEnvironmentPermissions`

For records and uploads in the *current* environment, the SDK ships a higher-level helper:

```ts
client.roles.updateCurrentEnvironmentPermissions(roleId, {
  positive_item_type_permissions: { add: [...], remove: [...] },
  negative_item_type_permissions: { add: [...], remove: [...] },
  positive_upload_permissions:    { add: [...], remove: [...] },
  negative_upload_permissions:    { add: [...], remove: [...] },
});
```

It reads the role, applies the diff against the entries scoped to the current environment, and forwards the merged arrays — so individual entries can be added or removed without rewriting the surrounding state. Build trigger and search index permissions, and entries scoped to *other* environments, still need a direct `client.roles.update(...)` call.

###### Example Subtract one action from a role that grants "all"

Subtract a single action from a role that already grants `action: "all"`. The fix is to append a `negative_item_type_permissions` entry naming the action to take away — the existing positive `all` entry stays, and the formula `(positive_*) − negative_*` resolves to "everything but `delete`".

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Look up the existing "Power editor" role to patch.
  const allRoles = await client.roles.list();
  const role = allRoles.find((candidate) => candidate.name === "Power editor")!;

  // Append a negative entry forbidding `delete` to the current environment.
  const updated = await client.roles.updateCurrentEnvironmentPermissions(
    role.id,
    {
      negative_item_type_permissions: {
        add: [
          {
            action: "delete",
            on_creator: "anyone",
          },
        ],
      },
    },
  );

  console.log("Updated role:", updated.id, "—", updated.name);
  console.log(
    "Negative permissions now:",
    JSON.stringify(updated.negative_item_type_permissions, null, 2),
  );
}

run();
```

Returned output

```javascript
Updated role: 443075 — Power editor
Negative permissions now: [
  {
    "environment": "main",
    "item_type": null,
    "workflow": null,
    "on_stage": null,
    "to_stage": null,
    "action": "delete",
    "on_creator": "anyone",
    "localization_scope": null,
    "locale": null
  }
]
```

## Effects on bound credentials

Changes take effect immediately for every credential bound to this role: collaborators, SSO users, and API tokens will see new requests evaluated against the updated `meta.final_permissions` on their next call.

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"Editor"`

The name of the role

**`can_edit_favicon`**

- Optional
- Type: boolean

Can edit favicon, global SEO settings and no-index policy

**`can_edit_site`**

- Optional
- Type: boolean

Can change project-wide settings (project name, internal subdomain, frontend preview URL, deployment settings)

**`can_edit_schema`**

- Optional
- Type: boolean

Can create and edit the project schema: models, block models, fields, fieldsets, validators, and plugins

**`can_manage_menu`**

- Optional
- Type: boolean

Can customize content navigation bar

**`can_edit_environment`**

- Optional
- Type: boolean

Can edit per-environment settings of the environments this role has access to: locales, timezone, and UI theme. This is *not* about creating or switching environments — see `can_manage_environments` for that, and `environments_access` for which environments this role can enter at all.

**`can_promote_environments`**

- Optional
- Type: boolean

Can promote a sandbox environment to primary (atomic swap) and toggle the project's maintenance mode. Distinct from `can_manage_environments`, which covers creating/forking/deleting sandboxes.

**`environments_access`**

- Optional
- Type: enum
- Example: `"primary_only"`

Specifies the environments the user can access

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Grants access to all environments

**`primary_only`**

- Optional

Grants access exclusively to the primary environment

**`sandbox_only`**

- Optional

Grants access exclusively to sandbox environments

**`none`**

- Optional

No access to any environment. This value is typically used when the role is intended to inherit access settings from other roles

</details>

**`can_manage_users`**

- Optional
- Type: boolean

Can create and edit roles and invite/remove collaborators

**`can_manage_shared_filters`**

- Optional
- Type: boolean

Can create and edit shared filters (both for models and the media area)

**`can_manage_search_indexes`**

- Optional
- Type: boolean

Can create and edit search indexes

**`can_manage_upload_collections`**

- Optional
- Type: boolean

Can create and edit upload collections

**`can_manage_build_triggers`**

- Optional
- Type: boolean

Can create and edit build triggers

**`can_manage_webhooks`**

- Optional
- Type: boolean

Can create and edit webhooks

**`can_manage_environments`**

- Optional
- Type: boolean

Can create, fork, and delete sandbox environments. Promotion to primary is gated separately by `can_promote_environments`.

**`can_manage_sso`**

- Optional
- Type: boolean

Can manage Single Sign-On settings

**`can_access_audit_log`**

- Optional
- Type: boolean

Can access Audit Log

**`can_manage_workflows`**

- Optional
- Type: boolean

Can create and edit workflows

**`can_manage_access_tokens`**

- Optional
- Type: boolean

Can manage API tokens

**`can_perform_site_search`**

- Optional
- Type: boolean

Can perform Site Search API calls

**`can_access_build_events_log`**

- Optional
- Type: boolean

Can access the build events log

**`can_access_search_index_events_log`**

- Optional
- Type: boolean

Can access the search index events log

**`positive_item_type_permissions`**

- Optional
- Type: Array\<object\>

Allowed actions on a model (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). Idiomatic recipes:
- To grant every action, use a single `action: "all"` entry with `localization_scope: "all"`.
- To grant a subset (e.g. create+read+update but not delete), prefer a single `action: "all"` entry plus `negative_item_type_permissions` entries for the actions to exclude — instead of listing each allowed action separately.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`negative_item_type_permissions`**

- Optional
- Type: Array\<object\>

Prohibited actions on a model (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions (e.g. forbid `delete`).

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`positive_upload_permissions`**

- Optional
- Type: Array\<object\>

Allowed actions on uploads (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). To grant a subset, prefer a single `action: "all"` entry plus `negative_upload_permissions` entries for the actions to exclude.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`negative_upload_permissions`**

- Optional
- Type: Array\<object\>

Prohibited actions on uploads (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`positive_build_trigger_permissions`**

- Optional
- Type: Array\<object\>

Build triggers this role is allowed to **manually fire**. An entry with `build_trigger: null` covers every build trigger. Note: this does not control creating/editing build triggers themselves — that is gated by `can_manage_build_triggers`.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`negative_build_trigger_permissions`**

- Optional
- Type: Array\<object\>

Build triggers this role is **forbidden** from manually firing. Negative entries take precedence over positive ones; pair with a `build_trigger: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`positive_search_index_permissions`**

- Optional
- Type: Array\<object\>

Search indexes this role is allowed to **manually re-index**. An entry with `search_index: null` covers every search index. Note: this does not control creating/editing search indexes themselves — that is gated by `can_manage_search_indexes`.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

**`negative_search_index_permissions`**

- Optional
- Type: Array\<object\>

Search indexes this role is **forbidden** from manually re-indexing. Negative entries take precedence over positive ones; pair with a `search_index: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

**`meta.final_permissions`**

- Optional
- Type: object

The final set of permissions considering also inherited roles

<details>
<summary>Show object format</summary>

**`can_edit_site`**

- Required
- Type: boolean

Can change project-wide settings (project name, internal subdomain, frontend preview URL, deployment settings)

**`can_edit_favicon`**

- Required
- Type: boolean

Can edit favicon, global SEO settings and no-index policy

**`can_edit_schema`**

- Required
- Type: boolean

Can create and edit the project schema: models, block models, fields, fieldsets, validators, and plugins

**`can_manage_menu`**

- Required
- Type: boolean

Can customize content navigation bar

**`can_manage_users`**

- Required
- Type: boolean

Can create and edit roles and invite/remove collaborators

**`can_manage_environments`**

- Required
- Type: boolean

Can create, fork, and delete sandbox environments. Promotion to primary is gated separately by `can_promote_environments`.

**`can_manage_webhooks`**

- Required
- Type: boolean

Can create and edit webhooks

**`environments_access`**

- Required
- Type: enum
- Example: `"primary_only"`

Specifies the environments the user can access

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Grants access to all environments

**`primary_only`**

- Optional

Grants access exclusively to the primary environment

**`sandbox_only`**

- Optional

Grants access exclusively to sandbox environments

**`none`**

- Optional

No access to any environment. This value is typically used when the role is intended to inherit access settings from other roles

</details>

**`can_manage_sso`**

- Required
- Type: boolean

Can manage Single Sign-On settings

**`can_access_audit_log`**

- Required
- Type: boolean

Can access Audit Log

**`can_manage_workflows`**

- Required
- Type: boolean

Can create and edit workflows

**`can_edit_environment`**

- Required
- Type: boolean

Can edit per-environment settings of the environments this role has access to: locales, timezone, and UI theme. This is *not* about creating or switching environments — see `can_manage_environments` for that, and `environments_access` for which environments this role can enter at all.

**`can_promote_environments`**

- Required
- Type: boolean

Can promote a sandbox environment to primary (atomic swap) and toggle the project's maintenance mode. Distinct from `can_manage_environments`, which covers creating/forking/deleting sandboxes.

**`can_manage_shared_filters`**

- Required
- Type: boolean

Can create and edit shared filters (both for models and the media area)

**`can_manage_search_indexes`**

- Required
- Type: boolean

Can create and edit search indexes

**`can_manage_build_triggers`**

- Required
- Type: boolean

Can create and edit build triggers

**`can_manage_upload_collections`**

- Required
- Type: boolean

Can create and edit upload collections

**`can_manage_access_tokens`**

- Required
- Type: boolean

Can manage API tokens

**`can_perform_site_search`**

- Required
- Type: boolean

Can perform Site Search API calls

**`can_access_build_events_log`**

- Required
- Type: boolean

Can access the build events log

**`can_access_search_index_events_log`**

- Required
- Type: boolean

Can access the search index events log

**`positive_item_type_permissions`**

- Required
- Type: Array\<object\>

Allowed actions on a model (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). Idiomatic recipes:
- To grant every action, use a single `action: "all"` entry with `localization_scope: "all"`.
- To grant a subset (e.g. create+read+update but not delete), prefer a single `action: "all"` entry plus `negative_item_type_permissions` entries for the actions to exclude — instead of listing each allowed action separately.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`negative_item_type_permissions`**

- Required
- Type: Array\<object\>

Prohibited actions on a model (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions (e.g. forbid `delete`).

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

<details>
<summary>Show object format when action is "read"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "update" or "publish"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

**`publish`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Content under a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "duplicate"</summary>

**`action`**

- Required
- Type: enum
- Example: `"duplicate"`

Permitted action

<details>
<summary>Show enum values</summary>

**`duplicate`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "delete" or "edit_creator" or "take_over"</summary>

**`action`**

- Required
- Type: enum
- Example: `"delete"`

Permitted action

<details>
<summary>Show enum values</summary>

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`take_over`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

</details>

<details>
<summary>Show object format when action is "move_to_stage"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move_to_stage"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move_to_stage`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`item_type`**

- Optional
- Type: string, null

Restricts the permission to a specific model. When `null`, the permission applies to all models.

**`workflow`**

- Optional
- Type: string, null

Restricts the permission to records associated with a specific workflow. Mutually exclusive with `item_type`.

**`on_stage`**

- Optional
- Type: string, null

Restrict to records currently on a workflow stage.

**`to_stage`**

- Optional
- Type: string, null

Restrict to moves towards a specific workflow stage.

</details>

**`positive_upload_permissions`**

- Required
- Type: Array\<object\>

Allowed actions on uploads (or all) for a role.

The shape of each entry depends on the `action` (discriminated union). To grant a subset, prefer a single `action: "all"` entry plus `negative_upload_permissions` entries for the actions to exclude.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`negative_upload_permissions`**

- Required
- Type: Array\<object\>

Prohibited actions on uploads (or all) for a role. Negative permissions take precedence and are typically paired with a broader positive `action: "all"` entry to subtract specific actions.

<details>
<summary>Show object format when action is "all"</summary>

**`action`**

- Required
- Type: enum
- Example: `"all"`

Permitted action

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

For `action: "all"` this must be `"all"`.

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "update"</summary>

**`action`**

- Required
- Type: enum
- Example: `"update"`

Permitted action

<details>
<summary>Show enum values</summary>

**`update`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`localization_scope`**

- Required
- Type: enum
- Example: `"all"`

Permitted content scope

<details>
<summary>Show enum values</summary>

**`all`**

- Optional

Any content (localized/unlocalized)

**`localized`**

- Optional

Localized content in a specific locale (`locale` must be defined)

**`not_localized`**

- Optional

Non-localized content

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`locale`**

- Optional
- Type: string, null
- Example: `"en"`

Required (non-null) when `localization_scope` is `"localized"`; must be omitted otherwise.

</details>

<details>
<summary>Show object format when action is "create"</summary>

**`action`**

- Required
- Type: enum
- Example: `"create"`

Permitted action

<details>
<summary>Show enum values</summary>

**`create`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "read" or "delete" or "edit_creator" or "replace_asset"</summary>

**`action`**

- Required
- Type: enum
- Example: `"read"`

Permitted action

<details>
<summary>Show enum values</summary>

**`read`**

- Optional

**`delete`**

- Optional

**`edit_creator`**

- Optional

**`replace_asset`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

</details>

<details>
<summary>Show object format when action is "move"</summary>

**`action`**

- Required
- Type: enum
- Example: `"move"`

Permitted action

<details>
<summary>Show enum values</summary>

**`move`**

- Optional

</details>

**`environment`**

- Required
- Type: string
- Example: `"main"`

ID of environment. Can only contain lowercase letters, numbers and dashes

**`on_creator`**

- Required
- Type: enum
- Example: `"anyone"`

Permitted creator

<details>
<summary>Show enum values</summary>

**`anyone`**

- Optional

Created by anyone

**`self`**

- Optional

Created by the user itself

**`role`**

- Optional

Created by a user with the same role

</details>

**`upload_collection`**

- Optional
- Type: string, null

Restricts the permission to a specific upload collection. When `null`, the permission applies to all collections.

**`move_to_upload_collection`**

- Optional
- Type: string, null

Restricts the destination upload collection of the move action. When `null`, any destination is allowed.

</details>

**`positive_build_trigger_permissions`**

- Required
- Type: Array\<object\>

Build triggers this role is allowed to **manually fire**. An entry with `build_trigger: null` covers every build trigger. Note: this does not control creating/editing build triggers themselves — that is gated by `can_manage_build_triggers`.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`negative_build_trigger_permissions`**

- Required
- Type: Array\<object\>

Build triggers this role is **forbidden** from manually firing. Negative entries take precedence over positive ones; pair with a `build_trigger: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`build_trigger`**

- Optional
- Type: string, null

</details>

**`positive_search_index_permissions`**

- Required
- Type: Array\<object\>

Search indexes this role is allowed to **manually re-index**. An entry with `search_index: null` covers every search index. Note: this does not control creating/editing search indexes themselves — that is gated by `can_manage_search_indexes`.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

**`negative_search_index_permissions`**

- Required
- Type: Array\<object\>

Search indexes this role is **forbidden** from manually re-indexing. Negative entries take precedence over positive ones; pair with a `search_index: null` positive entry to allow all-but-N.

<details>
<summary>Show objects format inside array</summary>

**`search_index`**

- Optional
- Type: string, null

</details>

</details>

**`inherits_permissions_from`**

- Optional
- Type: Array<[ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)>

The roles from which this role inherits permissions

## Returns

Returns a resource object of type [role](/docs/content-management-api/resources/role.md)

---

# Content Management API — List all roles

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/role/instances.md

Lists every role defined on the project, including the built-in factory roles (e.g. *Admin*, *Editor*) and any custom ones.

Each entry includes both the directly-declared `attributes` and the inheritance-aware `meta.final_permissions` (see the [Retrieve a role](/docs/content-management-api/resources/role/self.md) endpoint for the difference).

## Returns

Returns an array of resource objects of type [role](/docs/content-management-api/resources/role.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const roles = await client.roles.list();

  for (const role of roles) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(role);
  }
}

run();
```

Returned output

```javascript
{
  id: "34",
  name: "Editor",
  can_edit_site: true,
  can_edit_favicon: true,
  can_edit_schema: true,
  can_manage_menu: true,
  can_manage_users: true,
  can_manage_shared_filters: true,
  can_manage_search_indexes: true,
  can_manage_upload_collections: true,
  can_manage_environments: true,
  can_manage_webhooks: true,
  environments_access: "primary_only",
  can_manage_sso: true,
  can_access_audit_log: true,
  can_manage_workflows: true,
  can_edit_environment: true,
  can_promote_environments: true,
  can_manage_build_triggers: true,
  can_manage_access_tokens: true,
  can_perform_site_search: true,
  can_access_build_events_log: true,
  can_access_search_index_events_log: true,
  positive_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  negative_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  positive_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  negative_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  positive_build_trigger_permissions: [{}],
  negative_build_trigger_permissions: [{}],
  positive_search_index_permissions: [{}],
  negative_search_index_permissions: [{}],
  meta: {
    final_permissions: {
      can_edit_site: true,
      can_edit_favicon: true,
      can_edit_schema: true,
      can_manage_menu: true,
      can_manage_users: true,
      can_manage_environments: true,
      can_manage_webhooks: true,
      environments_access: "primary_only",
      can_manage_sso: true,
      can_access_audit_log: true,
      can_manage_workflows: true,
      can_edit_environment: true,
      can_promote_environments: true,
      can_manage_shared_filters: true,
      can_manage_search_indexes: true,
      can_manage_build_triggers: true,
      can_manage_upload_collections: true,
      can_manage_access_tokens: true,
      can_perform_site_search: true,
      can_access_build_events_log: true,
      can_access_search_index_events_log: true,
      positive_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      negative_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      positive_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      negative_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      positive_build_trigger_permissions: [{}],
      negative_build_trigger_permissions: [{}],
      positive_search_index_permissions: [{}],
      negative_search_index_permissions: [{}],
    },
  },
  inherits_permissions_from: [{ type: "role", id: "34" }],
}
```

---

# Content Management API — Retrieve a role

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/role/self.md

Returns a single role by id. The response includes both the directly-declared `attributes.*` and the inheritance-aware `meta.final_permissions` — see the [Role resource overview](/docs/content-management-api/resources/role.md#effective-vs-declared-permissions) for the difference between the two.

## Returns

Returns a resource object of type [role](/docs/content-management-api/resources/role.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const roleId = "34";

  const role = await client.roles.find(roleId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(role);
}

run();
```

Returned output

```javascript
{
  id: "34",
  name: "Editor",
  can_edit_site: true,
  can_edit_favicon: true,
  can_edit_schema: true,
  can_manage_menu: true,
  can_manage_users: true,
  can_manage_shared_filters: true,
  can_manage_search_indexes: true,
  can_manage_upload_collections: true,
  can_manage_environments: true,
  can_manage_webhooks: true,
  environments_access: "primary_only",
  can_manage_sso: true,
  can_access_audit_log: true,
  can_manage_workflows: true,
  can_edit_environment: true,
  can_promote_environments: true,
  can_manage_build_triggers: true,
  can_manage_access_tokens: true,
  can_perform_site_search: true,
  can_access_build_events_log: true,
  can_access_search_index_events_log: true,
  positive_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  negative_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  positive_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  negative_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  positive_build_trigger_permissions: [{}],
  negative_build_trigger_permissions: [{}],
  positive_search_index_permissions: [{}],
  negative_search_index_permissions: [{}],
  meta: {
    final_permissions: {
      can_edit_site: true,
      can_edit_favicon: true,
      can_edit_schema: true,
      can_manage_menu: true,
      can_manage_users: true,
      can_manage_environments: true,
      can_manage_webhooks: true,
      environments_access: "primary_only",
      can_manage_sso: true,
      can_access_audit_log: true,
      can_manage_workflows: true,
      can_edit_environment: true,
      can_promote_environments: true,
      can_manage_shared_filters: true,
      can_manage_search_indexes: true,
      can_manage_build_triggers: true,
      can_manage_upload_collections: true,
      can_manage_access_tokens: true,
      can_perform_site_search: true,
      can_access_build_events_log: true,
      can_access_search_index_events_log: true,
      positive_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      negative_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      positive_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      negative_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      positive_build_trigger_permissions: [{}],
      negative_build_trigger_permissions: [{}],
      positive_search_index_permissions: [{}],
      negative_search_index_permissions: [{}],
    },
  },
  inherits_permissions_from: [{ type: "role", id: "34" }],
}
```

---

# Content Management API — Delete a role

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/role/destroy.md

> [!WARNING] ⚠️ Reassign credentials first
> A role cannot be deleted while collaborators, SSO users, or API tokens are still bound to it (nor while it's set as the project's SSO default role). Move every assignee to a different role before calling this endpoint, or the request will be rejected.

## Returns

Returns a resource object of type [role](/docs/content-management-api/resources/role.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const roleId = "34";

  const role = await client.roles.destroy(roleId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(role);
}

run();
```

Returned output

```javascript
{
  id: "34",
  name: "Editor",
  can_edit_site: true,
  can_edit_favicon: true,
  can_edit_schema: true,
  can_manage_menu: true,
  can_manage_users: true,
  can_manage_shared_filters: true,
  can_manage_search_indexes: true,
  can_manage_upload_collections: true,
  can_manage_environments: true,
  can_manage_webhooks: true,
  environments_access: "primary_only",
  can_manage_sso: true,
  can_access_audit_log: true,
  can_manage_workflows: true,
  can_edit_environment: true,
  can_promote_environments: true,
  can_manage_build_triggers: true,
  can_manage_access_tokens: true,
  can_perform_site_search: true,
  can_access_build_events_log: true,
  can_access_search_index_events_log: true,
  positive_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  negative_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  positive_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  negative_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  positive_build_trigger_permissions: [{}],
  negative_build_trigger_permissions: [{}],
  positive_search_index_permissions: [{}],
  negative_search_index_permissions: [{}],
  meta: {
    final_permissions: {
      can_edit_site: true,
      can_edit_favicon: true,
      can_edit_schema: true,
      can_manage_menu: true,
      can_manage_users: true,
      can_manage_environments: true,
      can_manage_webhooks: true,
      environments_access: "primary_only",
      can_manage_sso: true,
      can_access_audit_log: true,
      can_manage_workflows: true,
      can_edit_environment: true,
      can_promote_environments: true,
      can_manage_shared_filters: true,
      can_manage_search_indexes: true,
      can_manage_build_triggers: true,
      can_manage_upload_collections: true,
      can_manage_access_tokens: true,
      can_perform_site_search: true,
      can_access_build_events_log: true,
      can_access_search_index_events_log: true,
      positive_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      negative_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      positive_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      negative_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      positive_build_trigger_permissions: [{}],
      negative_build_trigger_permissions: [{}],
      positive_search_index_permissions: [{}],
      negative_search_index_permissions: [{}],
    },
  },
  inherits_permissions_from: [{ type: "role", id: "34" }],
}
```

---

# Content Management API — Duplicate a role

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/role/duplicate.md

Creates a new role that copies an existing one — its project-wide flags, `environments_access`, model permissions, upload permissions, build trigger permissions, and the `inherits_permissions_from` relationship are all carried over.

This is the most common starting point for a custom role: duplicate the closest-matching built-in role (e.g. *Editor*), then `PUT` the result with the few changes you actually need. Faster and less error-prone than reconstructing a permission tree from scratch.

The new role's `name` is automatically suffixed ( `(duplicate #N)`) to keep it unique within the project; rename it via the [Update endpoint](/docs/content-management-api/resources/role/update.md) right after duplicating.

## Returns

Returns a resource object of type [role](/docs/content-management-api/resources/role.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const roleId = "34";

  const role = await client.roles.duplicate(roleId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(role);
}

run();
```

Returned output

```javascript
{
  id: "34",
  name: "Editor",
  can_edit_site: true,
  can_edit_favicon: true,
  can_edit_schema: true,
  can_manage_menu: true,
  can_manage_users: true,
  can_manage_shared_filters: true,
  can_manage_search_indexes: true,
  can_manage_upload_collections: true,
  can_manage_environments: true,
  can_manage_webhooks: true,
  environments_access: "primary_only",
  can_manage_sso: true,
  can_access_audit_log: true,
  can_manage_workflows: true,
  can_edit_environment: true,
  can_promote_environments: true,
  can_manage_build_triggers: true,
  can_manage_access_tokens: true,
  can_perform_site_search: true,
  can_access_build_events_log: true,
  can_access_search_index_events_log: true,
  positive_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  negative_item_type_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "create", environment: "main", localization_scope: "all" },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "duplicate", environment: "main" },
    { action: "delete", environment: "main", on_creator: "anyone" },
    { action: "move_to_stage", environment: "main", on_creator: "anyone" },
  ],
  positive_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  negative_upload_permissions: [
    {
      action: "all",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    {
      action: "update",
      environment: "main",
      on_creator: "anyone",
      localization_scope: "all",
    },
    { action: "create", environment: "main" },
    { action: "read", environment: "main", on_creator: "anyone" },
    { action: "move", environment: "main", on_creator: "anyone" },
  ],
  positive_build_trigger_permissions: [{}],
  negative_build_trigger_permissions: [{}],
  positive_search_index_permissions: [{}],
  negative_search_index_permissions: [{}],
  meta: {
    final_permissions: {
      can_edit_site: true,
      can_edit_favicon: true,
      can_edit_schema: true,
      can_manage_menu: true,
      can_manage_users: true,
      can_manage_environments: true,
      can_manage_webhooks: true,
      environments_access: "primary_only",
      can_manage_sso: true,
      can_access_audit_log: true,
      can_manage_workflows: true,
      can_edit_environment: true,
      can_promote_environments: true,
      can_manage_shared_filters: true,
      can_manage_search_indexes: true,
      can_manage_build_triggers: true,
      can_manage_upload_collections: true,
      can_manage_access_tokens: true,
      can_perform_site_search: true,
      can_access_build_events_log: true,
      can_access_search_index_events_log: true,
      positive_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      negative_item_type_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "create", environment: "main", localization_scope: "all" },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "duplicate", environment: "main" },
        { action: "delete", environment: "main", on_creator: "anyone" },
        { action: "move_to_stage", environment: "main", on_creator: "anyone" },
      ],
      positive_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      negative_upload_permissions: [
        {
          action: "all",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        {
          action: "update",
          environment: "main",
          on_creator: "anyone",
          localization_scope: "all",
        },
        { action: "create", environment: "main" },
        { action: "read", environment: "main", on_creator: "anyone" },
        { action: "move", environment: "main", on_creator: "anyone" },
      ],
      positive_build_trigger_permissions: [{}],
      negative_build_trigger_permissions: [{}],
      positive_search_index_permissions: [{}],
      negative_search_index_permissions: [{}],
    },
  },
  inherits_permissions_from: [{ type: "role", id: "34" }],
}
```

---

# Content Management API — API token

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/access-token.md

An API token authenticates programmatic access to a project. Each token combines two layers of access control:

1.  A **Role** that defines what actions are permitted (the same Role resource used for human collaborators).
2.  A set of **API surface flags** (`can_access_cda`, `can_access_cda_preview`, `can_access_cma`) that gate which APIs the token can hit at all.

The token's effective capabilities are the *intersection* of the two.

> [!PROTIP] 💡 A CDA-only token can safely reuse a write-capable Role
> A token with only `can_access_cda: true` is safe to attach to a Role that grants `update`/`publish`/`delete` — the Content Delivery API exposes no write endpoints, so those actions have no surface to act on. This makes it practical to share a single Role definition between an editor (acting via the dashboard / CMA) and a public read token (used by a frontend / CDA) for the same project.

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of access_token

**`type`**

- Type: string

Must be exactly `"access_token"`.

**`name`**

- Type: string
- Example: `"Read-only API token"`

Name of API token

**`hardcoded_type`**

- Type: null, string

Internal marker for the project's built-in factory tokens (e.g. read-only API token), seeded by DatoCMS when the project is created. Read-only attribute. When non-null, attribute updates are rejected with `NON_EDITABLE_ACCESS_TOKEN`, but the token can still be deleted and regenerated. `null` for any token created via this API.

**`can_access_cda`**

- Type: boolean

Whether this API token can call the Content Delivery API (`graphql.datocms.com`) to fetch **published** content.

**`can_access_cda_preview`**

- Type: boolean

Whether this API token can call the Content Delivery API with the `X-Include-Drafts: true` header to fetch **draft** (current, unpublished) content. There is no separate endpoint — the CDA is a single GraphQL endpoint and this flag governs whether requesting drafts is allowed.

**`can_access_cma`**

- Type: boolean

Whether this API token can access the Content Management API

**`last_cma_access`**

- Type: enum, null
- Example: `"never"`

When this API token was last used to access the Content Management API. It returns `null` if the credentials you are using cannot manage the API tokens of the project.

<details>
<summary>Show enum values</summary>

**`today`**

Today

**`yesterday`**

Yesterday

**`this_week`**

This week (Monday-Sunday)

**`last_week`**

Last week (Monday-Sunday)

**`this_month`**

This calendar month

**`last_month`**

Last calendar month

**`never`**

No recent usage (beyond last month)

</details>

**`last_cda_access`**

- Type: enum, null
- Example: `"never"`

When this API token was last used to access the Content Delivery API. It returns `null` if the credentials you are using cannot manage the API tokens of the project.

<details>
<summary>Show enum values</summary>

**`today`**

Today

**`yesterday`**

Yesterday

**`this_week`**

This week (Monday-Sunday)

**`last_week`**

Last week (Monday-Sunday)

**`this_month`**

This calendar month

**`last_month`**

Last calendar month

**`never`**

No recent usage (beyond last month)

</details>

**`token`**

- Type: null, string
- Example: `"XXXXXXXXXXXXXXX"`

The secret value used as the `Authorization: Bearer <token>` credential. Returned on every endpoint (create, update, retrieve, list, rotate) to callers whose current role has `can_manage_access_tokens`; otherwise `null`.

**`role`**

- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md), null

Role

---

# Content Management API — Create a new API token

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/access-token/create.md

Creates a new API token for the project. Each token combines a **Role** (which actions are permitted) with a set of **API surface flags** (`can_access_cda`, `can_access_cda_preview`, `can_access_cma`) that gate which APIs the token can call at all. Effective capabilities are the *intersection* of the two layers.

> [!POSITIVE] ✅ A CDA-only token + write-capable role is safe by construction
> The Content Delivery API has no write endpoints. If a token has `can_access_cda: true` (and/or `can_access_cda_preview: true`) but `can_access_cma: false`, attaching it to a role with `update`/`publish`/`delete` permissions is harmless — those actions have no surface to act on. This is useful when you want to share a single Role definition between an editor (who acts via the dashboard / CMA) and the public-facing read token of the same project (used by a frontend / CDA).

The new token's secret is returned in `attributes.token` of the response (and on every subsequent read, as long as the caller has `can_manage_access_tokens`).

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Read-only API token"`

Name of API token

**`can_access_cda`**

- Required
- Type: boolean

Whether this API token can call the Content Delivery API (`graphql.datocms.com`) to fetch **published** content.

**`can_access_cda_preview`**

- Required
- Type: boolean

Whether this API token can call the Content Delivery API with the `X-Include-Drafts: true` header to fetch **draft** (current, unpublished) content. There is no separate endpoint — the CDA is a single GraphQL endpoint and this flag governs whether requesting drafts is allowed.

**`can_access_cma`**

- Required
- Type: boolean

Whether this API token can access the Content Management API

**`role`**

- Required
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Role

## Returns

Returns a resource object of type [access\_token](/docs/content-management-api/resources/access-token.md)

## Other examples

###### Example CDA-only token bound to a write-capable role

An API token bound to a **write-capable role**, but with the Content Management API surface *closed off*: only `can_access_cda` is enabled. Any attempt to use this token against `site-api.datocms.com` returns 401, while the same token can freely query `graphql.datocms.com` for published content.

The role on its own would let a credential edit, publish, and delete records. The CDA has no write endpoints, so attaching it here is harmless: the role's write permissions have no surface to act on. This is the safety-by-construction story called out on the [API token resource overview](/docs/content-management-api/resources/access-token.md).

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Look up the write-capable role this token will be bound to.
  const allRoles = await client.roles.list();
  const role = allRoles.find(
    (candidate) => candidate.name === "Editorial team",
  )!;

  // Create a token with the full role attached, but with the CMA closed off.
  // Result: this token can only fetch published content via the CDA — its
  // role's update/publish/delete capabilities have no surface to act on.
  const accessToken = await client.accessTokens.create({
    name: "Public CDA token",
    role: { type: "role", id: role.id },
    can_access_cda: true,
    can_access_cda_preview: false,
    can_access_cma: false,
  });

  console.log("Created token:", accessToken.id, "—", accessToken.name);
  console.log("Secret value:", accessToken.token);
  console.log("Effective surfaces: CDA only (CMA disabled)");
}

run();
```

Returned output

```javascript
Created token: 407203 — Public CDA token
Secret value: 427db8a23d5777bbb5bb363d405380
Effective surfaces: CDA only (CMA disabled)
```

---

# Content Management API — Update an API token

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/access-token/update.md

Updates an API token's name, role, or API surface flags. The token's secret value is not affected — to rotate it, use the [Rotate API token](/docs/content-management-api/resources/access-token/regenerate-token.md) endpoint.

If you omit `relationships` from the payload, the token's existing role is preserved. Send `relationships.role` only when you want to reassign the token to a different role.

Changes to the role or surface flags take effect immediately. A request that was permitted under the previous configuration may be rejected on the very next call once the new permissions have been written.

> [!WARNING] ⚠️ Hardcoded tokens cannot be edited
> The project's built-in factory tokens (those whose `attributes.hardcoded_type` is non-null) reject this endpoint with `NON_EDITABLE_ACCESS_TOKEN`. They can still be deleted or rotated.

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Read-only API token"`

Name of API token

**`can_access_cda`**

- Required
- Type: boolean

Whether this API token can call the Content Delivery API (`graphql.datocms.com`) to fetch **published** content.

**`can_access_cda_preview`**

- Required
- Type: boolean

Whether this API token can call the Content Delivery API with the `X-Include-Drafts: true` header to fetch **draft** (current, unpublished) content. There is no separate endpoint — the CDA is a single GraphQL endpoint and this flag governs whether requesting drafts is allowed.

**`can_access_cma`**

- Required
- Type: boolean

Whether this API token can access the Content Management API

**`role`**

- Optional
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Role

## Returns

Returns a resource object of type [access\_token](/docs/content-management-api/resources/access-token.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const accessTokenId = "312";

  const accessToken = await client.accessTokens.update(accessTokenId, {
    id: "312",
    name: "Read-only API token",
    can_access_cda: true,
    can_access_cda_preview: true,
    can_access_cma: true,
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(accessToken);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Read-only API token",
  hardcoded_type: "",
  can_access_cda: true,
  can_access_cda_preview: true,
  can_access_cma: true,
  last_cma_access: "never",
  last_cda_access: "never",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — List all API tokens

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/access-token/instances.md

Lists every API token defined on the project, including the built-in factory tokens (read-only, full-access, …) seeded by DatoCMS.

Each entry's `attributes.token` is included for callers whose role has `can_manage_access_tokens` (and is `null` otherwise). Use `attributes.last_cma_access` / `attributes.last_cda_access` to spot tokens that haven't been used recently and may be safe to revoke.

## Returns

Returns an array of resource objects of type [access\_token](/docs/content-management-api/resources/access-token.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const accessTokens = await client.accessTokens.list();

  for (const accessToken of accessTokens) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(accessToken);
  }
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Read-only API token",
  hardcoded_type: "",
  can_access_cda: true,
  can_access_cda_preview: true,
  can_access_cma: true,
  last_cma_access: "never",
  last_cda_access: "never",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Retrieve an API token

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/access-token/self.md

Returns a single API token by id, including its bound role, API surface flags, and — if the caller's role has `can_manage_access_tokens` — the `attributes.token` secret value. Without that permission, `attributes.token` is `null` and the rest of the payload is still returned.

## Returns

Returns a resource object of type [access\_token](/docs/content-management-api/resources/access-token.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const accessTokenId = "312";

  const accessToken = await client.accessTokens.find(accessTokenId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(accessToken);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Read-only API token",
  hardcoded_type: "",
  can_access_cda: true,
  can_access_cda_preview: true,
  can_access_cma: true,
  last_cma_access: "never",
  last_cda_access: "never",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Rotate API token

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/access-token/regenerate_token.md

Rotates the secret value of an API token. The role and API surface flags are preserved; only the `attributes.token` value changes.

> [!WARNING] ⚠️ The previous secret is invalidated immediately
> Any client still using the old value will start receiving `401 Unauthorized` on its very next request. Rotate during a deploy window where you can ship the new secret to all consumers atomically, or accept a brief outage for any caller you forget to update.

The new secret is returned in `attributes.token` of the response (and on every subsequent read, as long as the caller has `can_manage_access_tokens`).

## Returns

Returns a resource object of type [access\_token](/docs/content-management-api/resources/access-token.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const accessTokenId = "312";

  const accessToken = await client.accessTokens.regenerateToken(accessTokenId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(accessToken);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Read-only API token",
  hardcoded_type: "",
  can_access_cda: true,
  can_access_cda_preview: true,
  can_access_cma: true,
  last_cma_access: "never",
  last_cda_access: "never",
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Delete an API token

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/access-token/destroy.md

Deletes an API token. The secret is invalidated immediately and any client still using it will receive `401 Unauthorized` on its next request.

API tokens are first-class users in DatoCMS — they can own records, uploads, filters, and editing sessions. When the token to delete owns any such resources, the request **must** specify a destination owner via the `destination_user_type` (`user`, `sso_user`, `access_token`, or `account`) and `destination_user_id` query parameters; ownership is transferred to that user before the token is removed. If the token owns nothing, the parameters can be omitted.

A token cannot delete itself — this endpoint rejects requests authenticated with the very token being destroyed (`CANNOT_DESTROY_CURRENT_USER`). Use a different credential to revoke it.

## Query parameters

**`destination_user_type`**

- Type: enum
- Example: `"account"`

New owner for resources previously owned by the deleted access token. This argument specifies the new owner type. Use `account` or `organization` to reassign to the project's owner — `client.site.find().owner` returns the right type/id pair to pass.

<details>
<summary>Show enum values</summary>

**`account`**

**`organization`**

**`user`**

**`access_token`**

**`sso_user`**

</details>

**`destination_user_id`**

- Type: string
- Example: `"7865"`

New owner for resources previously owned by the deleted access token. This argument specifies the new owner ID.

## Returns

Returns a resource object of type [access\_token](/docs/content-management-api/resources/access-token.md)

## Other examples

###### Example Delete a token and reassign its owned resources

An API token may own resources — records it created, uploads it pushed, shared filters, editing sessions. Deleting the token transfers ownership of those resources to the user identified by the `destination_user_type` + `destination_user_id` query parameters before destroying the token.

This example reassigns to the project's **owner** — `client.site.find().owner` is always present and returns the `account` or `organization` directly, so it works as a universal fallback. Set `destination_user_type` to `user`, `sso_user`, or `access_token` instead to reassign to a specific collaborator or sibling token.

If you skip the parameters and the token still owns resources, the deletion will leave them orphaned. Always pass a destination unless you've verified the token owns nothing.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Look up the token we want to retire.
  const allTokens = await client.accessTokens.list();
  const tokenToDelete = allTokens.find(
    (candidate) => candidate.name === "Legacy CI token",
  )!;

  // The project's owner — an account or an organization — is always present
  // and is the safest fallback destination for orphaned resources.
  const site = await client.site.find();

  await client.accessTokens.destroy(tokenToDelete.id, {
    destination_user_type: site.owner.type,
    destination_user_id: site.owner.id,
  });

  console.log(
    `Deleted token ${tokenToDelete.id}; resources transferred to ${site.owner.type} ${site.owner.id}.`,
  );
}

run();
```

Returned output

```javascript
Deleted token 407202; resources transferred to organization 628404.
```

---

# Content Management API — Webhook

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook.md

A webhook allows to make requests following certain Dato events. It is linked to a Role, which describes what actions can be performed.

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of webhook

**`type`**

- Type: string

Must be exactly `"webhook"`.

**`name`**

- Type: string
- Example: `"Item type creation/update"`

Unique name for the webhook

**`url`**

- Type: string
- Example: `"https://www.example.com/webhook"`

The URL to be called

**`enabled`**

- Type: boolean

Whether the webhook is enabled and sending events or not

**`headers`**

- Type: null, object
- Example: `{ "X-Foo": "Bar" }`

Additional headers that will be sent. It returns `null` if the credentials you are using cannot manage the webhooks of the project.

**`events`**

- Type: Array\<object\>

<details>
<summary>Show objects format inside array</summary>

**`entity_type`**

- Type: enum
- Example: `"item"`

The subject of webhook triggering

<details>
<summary>Show enum values</summary>

**`item_type`**

**`item`**

**`upload`**

**`build_trigger`**

**`environment`**

**`maintenance_mode`**

**`sso_user`**

**`cda_cache_tags`**

</details>

**`event_types`**

- Type: Array\<string\>

**`filters`**

- Type: Array\<object\>, null

<details>
<summary>Show objects format inside array</summary>

**`entity_type`**

- Type: enum

<details>
<summary>Show enum values</summary>

**`item_type`**

**`item`**

**`build_trigger`**

**`environment`**

**`environment_type`**

</details>

**`entity_ids`**

- Type: Array\<string\>

</details>

</details>

**`http_basic_user`**

- Type: string, null
- Example: `"user"`

HTTP Basic Authorization username

**`http_basic_password`**

- Type: string, null
- Example: `"password"`

HTTP Basic Authorization password. It is `null` when the webhook has no basic auth, or when the credentials you are using cannot manage the webhooks of the project.

**`custom_payload`**

- Type: string, null
- Example: `'{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }'`

A custom payload

**`payload_api_version`**

- Type: string
- Example: `"3"`

Specifies which API version to use when serializing entities in the webhook payload

**`nested_items_in_payload`**

- Type: boolean

Whether the you want records present in the payload to show blocks expanded or not

**`auto_retry`**

- Type: boolean

If enabled, the system will attempt to retry the call several times when the webhook operation fails due to timeouts or errors.

---

# Content Management API — Create a new webhook

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook/create.md

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Item type creation/update"`

Unique name for the webhook

**`url`**

- Required
- Type: string
- Example: `"https://www.example.com/webhook"`

The URL to be called

**`headers`**

- Required
- Type: object
- Example: `{ "X-Foo": "Bar" }`

Additional headers that will be sent

**`events`**

- Required
- Type: Array\<object\>

<details>
<summary>Show objects format inside array</summary>

**`entity_type`**

- Required
- Type: enum
- Example: `"item"`

The subject of webhook triggering

<details>
<summary>Show enum values</summary>

**`item_type`**

- Optional

**`item`**

- Optional

**`upload`**

- Optional

**`build_trigger`**

- Optional

**`environment`**

- Optional

**`maintenance_mode`**

- Optional

**`sso_user`**

- Optional

**`cda_cache_tags`**

- Optional

</details>

**`event_types`**

- Required
- Type: Array\<string\>

**`filters`**

- Optional
- Type: Array\<object\>, null

<details>
<summary>Show objects format inside array</summary>

**`entity_type`**

- Required
- Type: enum

<details>
<summary>Show enum values</summary>

**`item_type`**

- Optional

**`item`**

- Optional

**`build_trigger`**

- Optional

**`environment`**

- Optional

**`environment_type`**

- Optional

</details>

**`entity_ids`**

- Required
- Type: Array\<string\>

</details>

</details>

**`custom_payload`**

- Required
- Type: string, null
- Example: `'{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }'`

A custom payload

**`http_basic_user`**

- Required
- Type: string, null
- Example: `"user"`

HTTP Basic Authorization username

**`http_basic_password`**

- Required
- Type: string, null
- Example: `"password"`

HTTP Basic Authorization password. It is `null` when the webhook has no basic auth, or when the credentials you are using cannot manage the webhooks of the project.

**`enabled`**

- Optional
- Type: boolean

Whether the webhook is enabled and sending events or not

**`payload_api_version`**

- Optional
- Type: string
- Example: `"3"`

Specifies which API version to use when serializing entities in the webhook payload

**`nested_items_in_payload`**

- Optional
- Type: boolean

Whether the you want records present in the payload to show blocks expanded or not

**`auto_retry`**

- Optional
- Type: boolean

If enabled, the system will attempt to retry the call several times when the webhook operation fails due to timeouts or errors.

## Returns

Returns a resource object of type [webhook](/docs/content-management-api/resources/webhook.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhook = await client.webhooks.create({
    name: "Item type creation/update",
    url: "https://www.example.com/webhook",
    headers: { "X-Foo": "Bar" },
    events: [{ entity_type: "item", event_types: ["update"] }],
    custom_payload:
      '{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }',
    http_basic_user: "user",
    http_basic_password: "password",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(webhook);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Item type creation/update",
  url: "https://www.example.com/webhook",
  enabled: true,
  headers: { "X-Foo": "Bar" },
  events: [{ entity_type: "item", event_types: ["update"] }],
  http_basic_user: "user",
  http_basic_password: "password",
  custom_payload: '{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }',
  payload_api_version: "3",
  nested_items_in_payload: true,
  auto_retry: true,
}
```

---

# Content Management API — Update a webhook

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook/update.md

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"Item type creation/update"`

Unique name for the webhook

**`url`**

- Optional
- Type: string
- Example: `"https://www.example.com/webhook"`

The URL to be called

**`custom_payload`**

- Optional
- Type: string, null
- Example: `'{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }'`

A custom payload

**`headers`**

- Optional
- Type: object
- Example: `{ "X-Foo": "Bar" }`

Additional headers that will be sent

**`events`**

- Optional
- Type: Array\<object\>

<details>
<summary>Show objects format inside array</summary>

**`entity_type`**

- Required
- Type: enum
- Example: `"item"`

The subject of webhook triggering

<details>
<summary>Show enum values</summary>

**`item_type`**

- Optional

**`item`**

- Optional

**`upload`**

- Optional

**`build_trigger`**

- Optional

**`environment`**

- Optional

**`maintenance_mode`**

- Optional

**`sso_user`**

- Optional

**`cda_cache_tags`**

- Optional

</details>

**`event_types`**

- Required
- Type: Array\<string\>

**`filters`**

- Optional
- Type: Array\<object\>, null

<details>
<summary>Show objects format inside array</summary>

**`entity_type`**

- Required
- Type: enum

<details>
<summary>Show enum values</summary>

**`item_type`**

- Optional

**`item`**

- Optional

**`build_trigger`**

- Optional

**`environment`**

- Optional

**`environment_type`**

- Optional

</details>

**`entity_ids`**

- Required
- Type: Array\<string\>

</details>

</details>

**`http_basic_user`**

- Optional
- Type: string, null
- Example: `"user"`

HTTP Basic Authorization username

**`http_basic_password`**

- Optional
- Type: string, null
- Example: `"password"`

HTTP Basic Authorization password. It is `null` when the webhook has no basic auth, or when the credentials you are using cannot manage the webhooks of the project.

**`enabled`**

- Optional
- Type: boolean

Whether the webhook is enabled and sending events or not

**`payload_api_version`**

- Optional
- Type: string
- Example: `"3"`

Specifies which API version to use when serializing entities in the webhook payload

**`nested_items_in_payload`**

- Optional
- Type: boolean

Whether the you want records present in the payload to show blocks expanded or not

**`auto_retry`**

- Optional
- Type: boolean

If enabled, the system will attempt to retry the call several times when the webhook operation fails due to timeouts or errors.

## Returns

Returns a resource object of type [webhook](/docs/content-management-api/resources/webhook.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhookId = "312";

  const webhook = await client.webhooks.update(webhookId, { id: "312" });

  // Check the 'Returned output' tab for the result ☝️
  console.log(webhook);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Item type creation/update",
  url: "https://www.example.com/webhook",
  enabled: true,
  headers: { "X-Foo": "Bar" },
  events: [{ entity_type: "item", event_types: ["update"] }],
  http_basic_user: "user",
  http_basic_password: "password",
  custom_payload: '{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }',
  payload_api_version: "3",
  nested_items_in_payload: true,
  auto_retry: true,
}
```

---

# Content Management API — List all webhooks

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook/instances.md

## Returns

Returns an array of resource objects of type [webhook](/docs/content-management-api/resources/webhook.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhooks = await client.webhooks.list();

  for (const webhook of webhooks) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(webhook);
  }
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Item type creation/update",
  url: "https://www.example.com/webhook",
  enabled: true,
  headers: { "X-Foo": "Bar" },
  events: [{ entity_type: "item", event_types: ["update"] }],
  http_basic_user: "user",
  http_basic_password: "password",
  custom_payload: '{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }',
  payload_api_version: "3",
  nested_items_in_payload: true,
  auto_retry: true,
}
```

---

# Content Management API — Retrieve a webhook

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook/self.md

## Returns

Returns a resource object of type [webhook](/docs/content-management-api/resources/webhook.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhookId = "312";

  const webhook = await client.webhooks.find(webhookId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(webhook);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Item type creation/update",
  url: "https://www.example.com/webhook",
  enabled: true,
  headers: { "X-Foo": "Bar" },
  events: [{ entity_type: "item", event_types: ["update"] }],
  http_basic_user: "user",
  http_basic_password: "password",
  custom_payload: '{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }',
  payload_api_version: "3",
  nested_items_in_payload: true,
  auto_retry: true,
}
```

---

# Content Management API — Delete a webhook

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook/destroy.md

## Returns

Returns a resource object of type [webhook](/docs/content-management-api/resources/webhook.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhookId = "312";

  const webhook = await client.webhooks.destroy(webhookId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(webhook);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Item type creation/update",
  url: "https://www.example.com/webhook",
  enabled: true,
  headers: { "X-Foo": "Bar" },
  events: [{ entity_type: "item", event_types: ["update"] }],
  http_basic_user: "user",
  http_basic_password: "password",
  custom_payload: '{ "message": "{{event_type}} event triggered on {{entity_type}}!", "entity_id": "{{#entity}}{{id}}{{/entity}}"] }',
  payload_api_version: "3",
  nested_items_in_payload: true,
  auto_retry: true,
}
```

---

# Content Management API — Webhook call

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook-call.md

This represents a log entry in the webhooks activity list, detailing a specific webhook event along with its delivery attempt information.

## Object payload

**`id`**

- Type: string
- Example: `"42"`

ID of webhook call

**`type`**

- Type: string

Must be exactly `"webhook_call"`.

**`entity_type`**

- Type: enum
- Example: `"item"`

The subject of webhook triggering

<details>
<summary>Show enum values</summary>

**`item_type`**

**`item`**

**`upload`**

**`build_trigger`**

**`environment`**

**`maintenance_mode`**

**`sso_user`**

**`cda_cache_tags`**

</details>

**`event_type`**

- Type: enum
- Example: `"update"`

The event that triggers the webhook call

<details>
<summary>Show enum values</summary>

**`create`**

**`update`**

**`delete`**

**`publish`**

**`unpublish`**

**`promote`**

**`deploy_started`**

**`deploy_succeeded`**

**`deploy_failed`**

**`change`**

**`invalidate`**

</details>

**`created_at`**

- Type: date-time
- Example: `"2016-09-20T18:50:24.914Z"`

The moment the event was created

**`request_url`**

- Type: string
- Example: `"https://www.example.com/webhook"`

The url that the webhook called

**`request_headers`**

- Type: object

The request's headers

Example:

```json
{
  Accept: "*/*",
  "User-Agent": "DatoCMS (datocms.com)",
  Authorization: "Basic Y2lhbzptaWFv",
  "Content-Type": "application/json",
}
```

**`request_payload`**

- Type: string
- Example: `'{"webhook_call_id":"103216210","event_triggered_at":"2024-08-26T12:49:16Z","attempted_auto_retries_count":0,"webhook_id":"28374","site_id":"205","environment":"main","is_environment_primary":true,"entity_type":"maintenance_mode","event_type":"change","entity":{"id":"maintenance_mode","type":"maintenance_mode","attributes":{"active":false}},"related_entities":[]}'`

The webhook's request payload is encoded as a string. Use `JSON.parse()` to parse it.

**`response_status`**

- Type: integer, null
- Example: `200`

The status of the response

**`response_headers`**

- Type: object, null

The response's headers

Example:

```json
{
  via: "1.1 vegur, 1.1 37c0945d19329fccc23efb283d01aa06.cloudfront.net (CloudFront)",
  date: "Fri, 27 Jul 2018 11:59:20 GMT",
  server: "gunicorn/19.6.0",
}
```

**`response_payload`**

- Type: string, null
- Example: `"ok"`

The body of the response

**`attempted_auto_retries_count`**

- Type: integer
- Example: `2`

The number of retries attempted so far

**`last_sent_at`**

- Type: date-time
- Example: `"2016-09-20T18:50:24.914Z"`

The last moment the call occurred

**`next_retry_at`**

- Type: date-time, null
- Example: `"2016-09-20T18:50:24.914Z"`

The date when the next retry attempt is scheduled to run. If no retry attempt is scheduled, it is set to null

**`status`**

- Type: enum
- Example: `"success"`

The current status

<details>
<summary>Show enum values</summary>

**`pending`**

The delivery attempt is currently in process

**`success`**

Delivery completed successfully

**`failed`**

Delivery attempt(s) failed due to errors/timeouts

**`rescheduled`**

The last delivery attempt failed, a new one will be retried later

</details>

**`webhook`**

- Type: [ResourceLinkage\<"webhook"\>](https://www-draft.datocms.com/docs/content-management-api/resources/webhook.md)

The webhook which has been called

---

# Content Management API — List all webhooks calls

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook-call/instances.md

## Query parameters

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 30, maximum is 500)

</details>

**`filter`**

- Type: object

Attributes to filter

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"42,554"`

IDs to fetch, comma separated

**`fields`**

- Type: object

<details>
<summary>Show object format</summary>

**`webhook_id`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: string

</details>

**`entity_type`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: enum
- Example: `"item"`

The subject of webhook triggering

<details>
<summary>Show enum values</summary>

**`item_type`**

**`item`**

**`upload`**

**`build_trigger`**

**`environment`**

**`maintenance_mode`**

**`sso_user`**

**`cda_cache_tags`**

</details>

</details>

**`event_type`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: enum
- Example: `"update"`

The event that triggers the webhook call

<details>
<summary>Show enum values</summary>

**`create`**

**`update`**

**`delete`**

**`publish`**

**`unpublish`**

**`promote`**

**`deploy_started`**

**`deploy_succeeded`**

**`deploy_failed`**

**`change`**

**`invalidate`**

</details>

</details>

**`status`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: enum
- Example: `"success"`

The current status

<details>
<summary>Show enum values</summary>

**`pending`**

The delivery attempt is currently in process

**`success`**

Delivery completed successfully

**`failed`**

Delivery attempt(s) failed due to errors/timeouts

**`rescheduled`**

The last delivery attempt failed, a new one will be retried later

</details>

</details>

**`last_sent_at`**

- Type: object

<details>
<summary>Show object format</summary>

**`gt`**

- Type: date-time

**`lt`**

- Type: date-time

</details>

**`next_retry_at`**

- Type: object

<details>
<summary>Show object format</summary>

**`gt`**

- Type: date-time

**`lt`**

- Type: date-time

</details>

**`created_at`**

- Type: object

<details>
<summary>Show object format</summary>

**`gt`**

- Type: date-time

**`lt`**

- Type: date-time

</details>

</details>

</details>

**`order_by`**

- Type: enum
- Example: `"created_at_desc"`

Fields used to order results

<details>
<summary>Show enum values</summary>

**`webhook_id_asc`**

**`webhook_id_desc`**

**`created_at_asc`**

**`created_at_desc`**

**`last_sent_at_asc`**

**`last_sent_at_desc`**

**`next_retry_at_asc`**

**`next_retry_at_desc`**

</details>

## Returns

Returns an array of resource objects of type [webhook\_call](/docs/content-management-api/resources/webhook-call.md)

## Other examples

###### Example List all calls

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhookCalls = await client.webhookCalls.list();
  console.log(webhookCalls);
}

run();
```

Returned output

```javascript
[
  {
    id: "84033321",
    type: "webhook_call",
    request_url: "https://www.example.com/webhook",
    request_payload: '{ \
      "environment": "main", \
      "entity_type": "item", \
      "event_type": "create", \
      "entity": { \
        "id": "Ke9nrZ4iRHWpKbJplG_zdQ", \
        "type": "item", \
        "attributes": { \
          "text_field": "Test" \
        }, \
        "relationships": { \
          "item_type": { \
            "data": { \
              "id": "Dq4WEbdjStWIeSeH_lBA-Q", \
              "type": "item_type" \
            } \
          }, \
          "creator": { \
            "data": { \
              "id": "104280", \
              "type": "account" \
            } \
          } \
        }, \
        "meta": { \
          "created_at": "2024-04-03T22:47:43.488+01:00", \
          "updated_at": "2024-04-03T22:47:43.496+01:00", \
          "published_at": "2024-04-03T22:47:43.532+01:00", \
          "publication_scheduled_at": null, \
          "unpublishing_scheduled_at": null, \
          "first_published_at": "2024-04-03T22:47:43.532+01:00", \
          "is_valid": true, \
          "is_current_version_valid": true, \
          "is_published_version_valid": true, \
          "status": "published", \
          "current_version": "QXuPXVc6SDmXcDh1MnImHQ", \
          "stage": null \
        } \
      }, \
      "related_entities": [ \
        { \
          "id": "Dq4WEbdjStWIeSeH_lBA-Q", \
          "type": "item_type", \
          "attributes": { \
            "name": "Example Model", \
            "singleton": true, \
            "sortable": false, \
            "api_key": "example_model", \
            "ordering_direction": null, \
            "ordering_meta": null, \
            "tree": false, \
            "modular_block": false, \
            "draft_mode_active": false, \
            "all_locales_required": false, \
            "collection_appearance": "table", \
            "has_singleton_item": true, \
            "hint": null, \
            "inverse_relationships_enabled": false \
          }, \
          "relationships": { \
            "fields": { \
              "data": [ \
                { \
                  "id": "TuzswqxpQXyzJGv_3JtBAA", \
                  "type": "field" \
                } \
              ] \
            }, \
            "fieldsets": { \
              "data": [] \
            }, \
            "singleton_item": { \
              "data": { \
                "id": "Ke9nrZ4iRHWpKbJplG_zdQ", \
                "type": "item" \
              } \
            }, \
            "ordering_field": { \
              "data": null \
            }, \
            "title_field": { \
              "data": { \
                "id": "TuzswqxpQXyzJGv_3JtBAA", \
                "type": "field" \
              } \
            }, \
            "image_preview_field": { \
              "data": null \
            }, \
            "excerpt_field": { \
              "data": null \
            }, \
            "workflow": { \
              "data": null \
            } \
          }, \
          "meta": { \
            "has_singleton_item": true \
          } \
        } \
      ] \
    }',
    request_headers: {
      Accept: "*/*",
      "X-Site-Id": "128378",
      "User-Agent": "DatoCMS (datocms.com)",
      "Content-Type": "application/json",
      "X-Webhook-Id": "27321",
      "X-Environment": "main",
    },
    response_status: 200,
    response_headers: {
      date: "Wed, 03 Apr 2024 21:47:44 GMT",
      "content-length": "2",
    },
    created_at: "2024-04-03T21:47:44.093Z",
    response_payload: "OK",
    entity_type: "item",
    event_type: "create",
    webhook: { id: "27321", type: "webhook" },
  },
  // etc
]
```


###### Example Filter webhook calls by environment

The `webhookCalls.list()` method does NOT support serverside filtering.

If you wish to filter the calls by some payload attribute, you must fetch them from the server and then filter them clientside, after decoding their `request_payload` fields with `JSON.parse()`.

This example shows how to filter the calls by their environment names.

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  // Make sure the API token has access to the CMA, and is stored securely
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // Which environment to look for
  const environmentNameToFilterBy = "main";

  // Because we can't filter serverside, we'll have to fetch all the calls and then deal with them clientside
  const iterator = await client.webhookCalls.listPagedIterator();

  // Empty array will be filled by the iterator
  const filteredWebhookCalls = [];

  // Go through the async iterable one at a time
  for await (const call of iterator) {
    // Parse the stringified webhook payload
    const payload = JSON.parse(call.request_payload);

    // Only push to the array if the environment matches
    const environment = payload.environment;
    if (environment === environmentNameToFilterBy) {
      filteredWebhookCalls.push(call);
    }
  }

  console.log(filteredWebhookCalls);
}

run();
```

Returned output

```javascript
[
  {
    id: "84033321",
    type: "webhook_call",
    request_url: "https://www.example.com/webhook",
    request_payload: '{ \
      "environment": "main", \
      "entity_type": "item", \
      "event_type": "create", \
      "entity": { \
        "id": "Ke9nrZ4iRHWpKbJplG_zdQ", \
        "type": "item", \
        "attributes": { \
          "text_field": "Test" \
        }, \
        "relationships": { \
          "item_type": { \
            "data": { \
              "id": "Dq4WEbdjStWIeSeH_lBA-Q", \
              "type": "item_type" \
            } \
          }, \
          "creator": { \
            "data": { \
              "id": "104280", \
              "type": "account" \
            } \
          } \
        }, \
        "meta": { \
          "created_at": "2024-04-03T22:47:43.488+01:00", \
          "updated_at": "2024-04-03T22:47:43.496+01:00", \
          "published_at": "2024-04-03T22:47:43.532+01:00", \
          "publication_scheduled_at": null, \
          "unpublishing_scheduled_at": null, \
          "first_published_at": "2024-04-03T22:47:43.532+01:00", \
          "is_valid": true, \
          "is_current_version_valid": true, \
          "is_published_version_valid": true, \
          "status": "published", \
          "current_version": "QXuPXVc6SDmXcDh1MnImHQ", \
          "stage": null \
        } \
      }, \
      "related_entities": [ \
        { \
          "id": "Dq4WEbdjStWIeSeH_lBA-Q", \
          "type": "item_type", \
          "attributes": { \
            "name": "Example Model", \
            "singleton": true, \
            "sortable": false, \
            "api_key": "example_model", \
            "ordering_direction": null, \
            "ordering_meta": null, \
            "tree": false, \
            "modular_block": false, \
            "draft_mode_active": false, \
            "all_locales_required": false, \
            "collection_appearance": "table", \
            "has_singleton_item": true, \
            "hint": null, \
            "inverse_relationships_enabled": false \
          }, \
          "relationships": { \
            "fields": { \
              "data": [ \
                { \
                  "id": "TuzswqxpQXyzJGv_3JtBAA", \
                  "type": "field" \
                } \
              ] \
            }, \
            "fieldsets": { \
              "data": [] \
            }, \
            "singleton_item": { \
              "data": { \
                "id": "Ke9nrZ4iRHWpKbJplG_zdQ", \
                "type": "item" \
              } \
            }, \
            "ordering_field": { \
              "data": null \
            }, \
            "title_field": { \
              "data": { \
                "id": "TuzswqxpQXyzJGv_3JtBAA", \
                "type": "field" \
              } \
            }, \
            "image_preview_field": { \
              "data": null \
            }, \
            "excerpt_field": { \
              "data": null \
            }, \
            "workflow": { \
              "data": null \
            } \
          }, \
          "meta": { \
            "has_singleton_item": true \
          } \
        } \
      ] \
    }',
    request_headers: {
      Accept: "*/*",
      "X-Site-Id": "128378",
      "User-Agent": "DatoCMS (datocms.com)",
      "Content-Type": "application/json",
      "X-Webhook-Id": "27321",
      "X-Environment": "main",
    },
    response_status: 200,
    response_headers: {
      date: "Wed, 03 Apr 2024 21:47:44 GMT",
      "content-length": "2",
    },
    created_at: "2024-04-03T21:47:44.093Z",
    response_payload: "OK",
    entity_type: "item",
    event_type: "create",
    webhook: { id: "27321", type: "webhook" },
  },
  // other calls filtered out, so only this one remains
]
```

---

# Content Management API — Retrieve a webhook call

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook-call/self.md

## Returns

Returns a resource object of type [webhook\_call](/docs/content-management-api/resources/webhook-call.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhookCallId = "42";

  const webhookCall = await client.webhookCalls.find(webhookCallId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(webhookCall);
}

run();
```

Returned output

```javascript
{
  id: "42",
  entity_type: "item",
  event_type: "update",
  created_at: "2016-09-20T18:50:24.914Z",
  request_url: "https://www.example.com/webhook",
  request_headers: {
    Accept: "*/*",
    "User-Agent": "DatoCMS (datocms.com)",
    Authorization: "Basic Y2lhbzptaWFv",
    "Content-Type": "application/json",
  },
  request_payload: '{"webhook_call_id":"103216210","event_triggered_at":"2024-08-26T12:49:16Z","attempted_auto_retries_count":0,"webhook_id":"28374","site_id":"205","environment":"main","is_environment_primary":true,"entity_type":"maintenance_mode","event_type":"change","entity":{"id":"maintenance_mode","type":"maintenance_mode","attributes":{"active":false}},"related_entities":[]}',
  response_status: 200,
  response_headers: {
    via: "1.1 vegur, 1.1 37c0945d19329fccc23efb283d01aa06.cloudfront.net (CloudFront)",
    date: "Fri, 27 Jul 2018 11:59:20 GMT",
    server: "gunicorn/19.6.0",
  },
  response_payload: "ok",
  attempted_auto_retries_count: 2,
  last_sent_at: "2016-09-20T18:50:24.914Z",
  next_retry_at: "2016-09-20T18:50:24.914Z",
  status: "success",
  webhook: { type: "webhook", id: "312" },
}
```

---

# Content Management API — Re-send the webhook call

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/webhook-call/resend_webhook.md

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const webhookCallId = "42";
  await client.webhookCalls.resendWebhook(webhookCallId);
}

run();
```

---

# Content Management API — Build trigger

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger.md

Configuration for different build triggers. You can have different staging and production environments in order to test your site before final deploy

## Object payload

**`id`**

- Type: string
- Example: `"1822"`

ID of build_trigger

**`type`**

- Type: string

Must be exactly `"build_trigger"`.

**`name`**

- Type: string
- Example: `"Custom build trigger"`

Name of the build trigger

**`adapter`**

- Type: enum
- Example: `"custom"`

The type of build trigger

<details>
<summary>Show enum values</summary>

**`custom`**

`adapter_settings` must include the following properties: `trigger_url`, `headers` and `payload`. The custom adapter also supports CircleCI webhooks for backward compatibility.

**`netlify`**

`adapter_settings` must include the following properties: `trigger_url`, `access_token`, `branch`, `site_id`

**`vercel`**

`adapter_settings` must include the following properties: `project_id`, `token`, `branch`, `team_id`, `deploy_hook_url`

**`gitlab`**

`adapter_settings` must include the following properties: `trigger_url`, `token`, `ref`, `build_parameters`

</details>

**`adapter_settings`**

- Type: null, object

Additional settings for the build trigger. The value depends on the `adapter`. It returns `null` if the credentials you are using cannot manage the build triggers of the project.

Example:

```json
{
  trigger_url: "http://some-url.com/trigger",
  headers: { Authorization: "Bearer abc123" },
  payload: { type: "build_request" },
}
```

**`last_build_completed_at`**

- Type: date-time, null
- Example: `"2017-03-30T09:29:14.872Z"`

Timestamp of the last build

**`build_status`**

- Type: string
- Example: `"success"`

Status of last build

**`webhook_token`**

- Type: null, string
- Example: `"xA1239ajsk123"`

Unique token for the webhook (it's the same token present in `webhook_url`). It returns `null` if the credentials you are using cannot manage the build triggers of the project.

**`webhook_url`**

- Type: null, string
- Example: `"https://webhooks.datocoms.com/xA1239ajsk123/deploy-results"`

The URL of the webhook your service has to call when the build completes to report it's status (success or error). It returns `null` if the credentials you are using cannot manage the build triggers of the project.

**`frontend_url`**

- Type: string, null
- Example: `"https://www.mywebsite.com/"`

The public URL of the frontend.

**`enabled`**

- Type: boolean

Whether the build trigger is enabled or not

**`autotrigger_on_scheduled_publications`**

- Type: boolean

Wheter an automatic build request to `webhook_url` should be made on scheduled publications/unpublishings

<details>
<summary>Show deprecated</summary>

**`indexing_status`**

- Deprecated
- Type: string
- Example: `"success"`

Status of Site Search for the frontend

Site Search features have been detached from build triggers. This attribute has no effect anymore: we keep it present for retrocompatibility. If you're programmatically using this field, please get in touch with support@datocms.com

**`indexing_enabled`**

- Deprecated
- Type: boolean

Wether Site Search is enabled or not. With Site Search, everytime the website is built, DatoCMS will respider it to get updated content

Site Search features have been detached from build triggers. This attribute has no effect anymore: we keep it present for retrocompatibility. If you're programmatically using this field, please get in touch with support@datocms.com

</details>

---

# Content Management API — List all build triggers for a site

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/instances.md

## Returns

Returns an array of resource objects of type [build\_trigger](/docs/content-management-api/resources/build-trigger.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggers = await client.buildTriggers.list();

  for (const buildTrigger of buildTriggers) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(buildTrigger);
  }
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Custom build trigger",
  adapter: "custom",
  adapter_settings: {
    trigger_url: "http://some-url.com/trigger",
    headers: { Authorization: "Bearer abc123" },
    payload: { type: "build_request" },
  },
  last_build_completed_at: "2017-03-30T09:29:14.872Z",
  build_status: "success",
  webhook_token: "xA1239ajsk123",
  webhook_url: "https://webhooks.datocoms.com/xA1239ajsk123/deploy-results",
  frontend_url: "https://www.mywebsite.com/",
  enabled: true,
  autotrigger_on_scheduled_publications: true,
}
```

---

# Content Management API — Retrieve a build trigger

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/self.md

## Returns

Returns a resource object of type [build\_trigger](/docs/content-management-api/resources/build-trigger.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggerId = "1822";

  const buildTrigger = await client.buildTriggers.find(buildTriggerId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(buildTrigger);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Custom build trigger",
  adapter: "custom",
  adapter_settings: {
    trigger_url: "http://some-url.com/trigger",
    headers: { Authorization: "Bearer abc123" },
    payload: { type: "build_request" },
  },
  last_build_completed_at: "2017-03-30T09:29:14.872Z",
  build_status: "success",
  webhook_token: "xA1239ajsk123",
  webhook_url: "https://webhooks.datocoms.com/xA1239ajsk123/deploy-results",
  frontend_url: "https://www.mywebsite.com/",
  enabled: true,
  autotrigger_on_scheduled_publications: true,
}
```

---

# Content Management API — Create build trigger

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/create.md

## Body parameters

**`name`**

- Required
- Type: string
- Example: `"Custom build trigger"`

Name of the build trigger

**`adapter`**

- Required
- Type: enum
- Example: `"custom"`

The type of build trigger

<details>
<summary>Show enum values</summary>

**`custom`**

- Optional

`adapter_settings` must include the following properties: `trigger_url`, `headers` and `payload`. The custom adapter also supports CircleCI webhooks for backward compatibility.

**`netlify`**

- Optional

`adapter_settings` must include the following properties: `trigger_url`, `access_token`, `branch`, `site_id`

**`vercel`**

- Optional

`adapter_settings` must include the following properties: `project_id`, `token`, `branch`, `team_id`, `deploy_hook_url`

**`gitlab`**

- Optional

`adapter_settings` must include the following properties: `trigger_url`, `token`, `ref`, `build_parameters`

</details>

**`frontend_url`**

- Required
- Type: string, null
- Example: `"https://www.mywebsite.com/"`

The public URL of the frontend.

**`adapter_settings`**

- Required
- Type: object

Additional settings for the build trigger. The value depends on the `adapter`.

Example:

```json
{
  trigger_url: "http://some-url.com/trigger",
  headers: { Authorization: "Bearer abc123" },
  payload: { type: "build_request" },
}
```

**`autotrigger_on_scheduled_publications`**

- Required
- Type: boolean

Wheter an automatic build request to `webhook_url` should be made on scheduled publications/unpublishings

**`webhook_token`**

- Optional
- Type: string
- Example: `"xA1239ajsk123"`

Unique token for the webhook (it's the same token present in `webhook_url`)

**`enabled`**

- Optional
- Type: boolean

Whether the build trigger is enabled or not

<details>
<summary>Show deprecated</summary>

**`indexing_enabled`**

- Deprecated
- Type: boolean

Wether Site Search is enabled or not. With Site Search, everytime the website is built, DatoCMS will respider it to get updated content

Site Search features have been detached from build triggers. This attribute has no effect anymore: we keep it present for retrocompatibility. If you're programmatically using this field, please get in touch with support@datocms.com

</details>

## Returns

Returns a resource object of type [build\_trigger](/docs/content-management-api/resources/build-trigger.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTrigger = await client.buildTriggers.create({
    name: "Custom build trigger",
    adapter: "custom",
    frontend_url: "https://www.mywebsite.com/",
    adapter_settings: {
      trigger_url: "http://some-url.com/trigger",
      headers: { Authorization: "Bearer abc123" },
      payload: { type: "build_request" },
    },
    autotrigger_on_scheduled_publications: true,
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(buildTrigger);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Custom build trigger",
  adapter: "custom",
  adapter_settings: {
    trigger_url: "http://some-url.com/trigger",
    headers: { Authorization: "Bearer abc123" },
    payload: { type: "build_request" },
  },
  last_build_completed_at: "2017-03-30T09:29:14.872Z",
  build_status: "success",
  webhook_token: "xA1239ajsk123",
  webhook_url: "https://webhooks.datocoms.com/xA1239ajsk123/deploy-results",
  frontend_url: "https://www.mywebsite.com/",
  enabled: true,
  autotrigger_on_scheduled_publications: true,
}
```

---

# Content Management API — Update build trigger

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/update.md

## Body parameters

**`name`**

- Optional
- Type: string
- Example: `"Custom build trigger"`

Name of the build trigger

**`adapter`**

- Optional
- Type: enum
- Example: `"custom"`

The type of build trigger

<details>
<summary>Show enum values</summary>

**`custom`**

- Optional

`adapter_settings` must include the following properties: `trigger_url`, `headers` and `payload`. The custom adapter also supports CircleCI webhooks for backward compatibility.

**`netlify`**

- Optional

`adapter_settings` must include the following properties: `trigger_url`, `access_token`, `branch`, `site_id`

**`vercel`**

- Optional

`adapter_settings` must include the following properties: `project_id`, `token`, `branch`, `team_id`, `deploy_hook_url`

**`gitlab`**

- Optional

`adapter_settings` must include the following properties: `trigger_url`, `token`, `ref`, `build_parameters`

</details>

**`enabled`**

- Optional
- Type: boolean

Whether the build trigger is enabled or not

**`frontend_url`**

- Optional
- Type: string, null
- Example: `"https://www.mywebsite.com/"`

The public URL of the frontend.

**`autotrigger_on_scheduled_publications`**

- Optional
- Type: boolean

Wheter an automatic build request to `webhook_url` should be made on scheduled publications/unpublishings

**`adapter_settings`**

- Optional
- Type: object

Additional settings for the build trigger. The value depends on the `adapter`.

Example:

```json
{
  trigger_url: "http://some-url.com/trigger",
  headers: { Authorization: "Bearer abc123" },
  payload: { type: "build_request" },
}
```

<details>
<summary>Show deprecated</summary>

**`indexing_enabled`**

- Deprecated
- Type: boolean

Wether Site Search is enabled or not. With Site Search, everytime the website is built, DatoCMS will respider it to get updated content

Site Search features have been detached from build triggers. This attribute has no effect anymore: we keep it present for retrocompatibility. If you're programmatically using this field, please get in touch with support@datocms.com

</details>

## Returns

Returns a resource object of type [build\_trigger](/docs/content-management-api/resources/build-trigger.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggerId = "1822";

  const buildTrigger = await client.buildTriggers.update(buildTriggerId, {
    id: "1822",
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(buildTrigger);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Custom build trigger",
  adapter: "custom",
  adapter_settings: {
    trigger_url: "http://some-url.com/trigger",
    headers: { Authorization: "Bearer abc123" },
    payload: { type: "build_request" },
  },
  last_build_completed_at: "2017-03-30T09:29:14.872Z",
  build_status: "success",
  webhook_token: "xA1239ajsk123",
  webhook_url: "https://webhooks.datocoms.com/xA1239ajsk123/deploy-results",
  frontend_url: "https://www.mywebsite.com/",
  enabled: true,
  autotrigger_on_scheduled_publications: true,
}
```

---

# Content Management API — Trigger a deploy

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/trigger.md

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggerId = "1822";
  await client.buildTriggers.trigger(buildTriggerId);
}

run();
```

---

# Content Management API — Abort a deploy and mark it as failed

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/abort.md

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggerId = "1822";
  await client.buildTriggers.abort(buildTriggerId);
}

run();
```

---

# Content Management API — Abort a site search spidering and mark it as failed

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/abort_indexing.md

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggerId = "1822";
  await client.buildTriggers.abortIndexing(buildTriggerId);
}

run();
```

---

# Content Management API — Trigger a new site search spidering of the website

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/reindex.md

By default a spidering of the site is performed automatically at the end of a deploy. If you only need the spidering without a deploy, you can trigger it by calling this endpoint.

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggerId = "1822";
  await client.buildTriggers.reindex(buildTriggerId);
}

run();
```

---

# Content Management API — Delete a build trigger

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-trigger/destroy.md

## Returns

Returns a resource object of type [build\_trigger](/docs/content-management-api/resources/build-trigger.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildTriggerId = "1822";

  const buildTrigger = await client.buildTriggers.destroy(buildTriggerId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(buildTrigger);
}

run();
```

Returned output

```javascript
{
  id: "1822",
  name: "Custom build trigger",
  adapter: "custom",
  adapter_settings: {
    trigger_url: "http://some-url.com/trigger",
    headers: { Authorization: "Bearer abc123" },
    payload: { type: "build_request" },
  },
  last_build_completed_at: "2017-03-30T09:29:14.872Z",
  build_status: "success",
  webhook_token: "xA1239ajsk123",
  webhook_url: "https://webhooks.datocoms.com/xA1239ajsk123/deploy-results",
  frontend_url: "https://www.mywebsite.com/",
  enabled: true,
  autotrigger_on_scheduled_publications: true,
}
```

---

# Content Management API — Deploy activity

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-event.md

Represents an event occurred during the deploy process of a build trigger.

## Object payload

**`id`**

- Type: string
- Example: `"34"`

ID of menu item

**`type`**

- Type: string

Must be exactly `"build_event"`.

**`event_type`**

- Type: enum
- Example: `"response_success"`

The type of activity

<details>
<summary>Show enum values</summary>

**`request_success`**

Build requested successfully

**`request_failure`**

Build request failed

**`response_success`**

Successful build notification

**`response_failure`**

Failed build notification

**`request_aborted`**

Build request aborted by user

**`response_unprocessable`**

Received notification is not valid

</details>

**`data`**

- Type: object

Any details regarding the event

Example:

```json
{
  request_body: '{"object_kind":"build","ref":"master","tag":false,"before_sha":"0000000000000000000000000000000000000000","sha":"ecfccf5ea28af900c14b499a2b762e029c7492","build_id":10495,"build_name":"build","build_stage":"test","build_status":"success","build_started_at":"2016-09-20 18:49:22 UTC","build_finished_at":"2016-09-20 18:50:24 UTC","build_duration":62.279854524,"build_allow_failure":false,"project_id":195,"project_id":"Stefano Verna / awesome-website","user":{"id":null,"name":null,"email":null},"commit":{"id":6754,"sha":"ecfccf5ea28af900c6614b499a2b762e029c7492","message":"Update gems\\n","author_name":"Stefano Verna","author_email":"s.verna@datocms.com","status":"success","duration":62,"started_at":"2016-09-20 18:49:22 UTC","finished_at":"2016-09-20 18:50:24 UTC"},"repository":{"name":"awesome-website","url":"git@gitlab.com:stefanoverna/awesome-website.git","description":"","visibility_level":0}}',
  request_headers: {
    Via: "1.1 vegur",
    Host: "webhooks.datocms.com",
    Origin: null,
    Version: "HTTP/1.1",
    Connection: "close",
    "Connect-Time": "0",
    "X-Request-Id": "5c1beced-0fe3-4c5b-b45d-68ba4a15b5f3",
    "X-Gitlab-Event": "Build Hook",
    "X-Forwarded-For": "46.101.135.219",
    "X-Request-Start": "1474397424903",
    "Total-Route-Time": "0",
    "X-Forwarded-Port": "443",
    "X-Forwarded-Proto": "https",
  },
}
```

**`created_at`**

- Type: date-time
- Example: `"2016-09-20T18:50:24.914Z"`

The moment the activity occurred

**`build_trigger`**

- Type: [ResourceLinkage\<"build_trigger"\>](https://www-draft.datocms.com/docs/content-management-api/resources/build_trigger.md)

Source build trigger

---

# Content Management API — List all deploy events

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-event/instances.md

## Query parameters

**`page`**

- Type: object

Parameters to control offset-based pagination

<details>
<summary>Show object format</summary>

**`offset`**

- Type: integer

The (zero-based) offset of the first entity returned in the collection (defaults to 0)

**`limit`**

- Type: integer

The maximum number of entities to return (defaults to 30, maximum is 500)

</details>

**`filter`**

- Type: object

Attributes to filter

<details>
<summary>Show object format</summary>

**`ids`**

- Type: string
- Example: `"42,554"`

IDs to fetch, comma separated

**`fields`**

- Type: object

<details>
<summary>Show object format</summary>

**`build_trigger_id`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: string

</details>

**`event_type`**

- Type: object

<details>
<summary>Show object format</summary>

**`eq`**

- Type: enum
- Example: `"response_success"`

The type of activity

<details>
<summary>Show enum values</summary>

**`request_success`**

Build requested successfully

**`request_failure`**

Build request failed

**`response_success`**

Successful build notification

**`response_failure`**

Failed build notification

**`request_aborted`**

Build request aborted by user

**`response_unprocessable`**

Received notification is not valid

</details>

</details>

**`created_at`**

- Type: object

<details>
<summary>Show object format</summary>

**`gt`**

- Type: date-time

**`lt`**

- Type: date-time

</details>

</details>

</details>

**`order_by`**

- Type: enum
- Example: `"created_at_desc"`

Fields used to order results

<details>
<summary>Show enum values</summary>

**`build_trigger_id_asc`**

**`build_trigger_id_desc`**

**`created_at_asc`**

**`created_at_desc`**

**`event_type_asc`**

**`event_type_desc`**

</details>

## Returns

Returns an array of resource objects of type [build\_event](/docs/content-management-api/resources/build-event.md)

## Examples

###### Example Basic example

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  // iterates over every page of results
  for await (const buildEvent of client.buildEvents.listPagedIterator()) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(buildEvent);
  }
}

run();
```

---

# Content Management API — Retrieve a deploy event

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/build-event/self.md

## Returns

Returns a resource object of type [build\_event](/docs/content-management-api/resources/build-event.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const buildEventId = "34";

  const buildEvent = await client.buildEvents.find(buildEventId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(buildEvent);
}

run();
```

Returned output

```javascript
{
  id: "34",
  event_type: "response_success",
  data: {
    request_body: '{"object_kind":"build","ref":"master","tag":false,"before_sha":"0000000000000000000000000000000000000000","sha":"ecfccf5ea28af900c14b499a2b762e029c7492","build_id":10495,"build_name":"build","build_stage":"test","build_status":"success","build_started_at":"2016-09-20 18:49:22 UTC","build_finished_at":"2016-09-20 18:50:24 UTC","build_duration":62.279854524,"build_allow_failure":false,"project_id":195,"project_id":"Stefano Verna / awesome-website","user":{"id":null,"name":null,"email":null},"commit":{"id":6754,"sha":"ecfccf5ea28af900c6614b499a2b762e029c7492","message":"Update gems\\n","author_name":"Stefano Verna","author_email":"s.verna@datocms.com","status":"success","duration":62,"started_at":"2016-09-20 18:49:22 UTC","finished_at":"2016-09-20 18:50:24 UTC"},"repository":{"name":"awesome-website","url":"git@gitlab.com:stefanoverna/awesome-website.git","description":"","visibility_level":0}}',
    request_headers: {
      Via: "1.1 vegur",
      Host: "webhooks.datocms.com",
      Origin: null,
      Version: "HTTP/1.1",
      Connection: "close",
      "Connect-Time": "0",
      "X-Request-Id": "5c1beced-0fe3-4c5b-b45d-68ba4a15b5f3",
      "X-Gitlab-Event": "Build Hook",
      "X-Forwarded-For": "46.101.135.219",
      "X-Request-Start": "1474397424903",
      "Total-Route-Time": "0",
      "X-Forwarded-Port": "443",
      "X-Forwarded-Proto": "https",
    },
  },
  created_at: "2016-09-20T18:50:24.914Z",
  build_trigger: { type: "build_trigger", id: "1822" },
}
```

---

# Content Management API — Subscription limit

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/subscription-limit.md

## Object payload

**`id`**

- Type: string
- Example: `"locales"`

ID of limit

**`type`**

- Type: string

Must be exactly `"subscription_limit"`.

**`code`**

- Type: string
- Example: `"users"`

The codename for the limit

**`usage`**

- Type: integer
- Example: `2`

Current usage

**`limit`**

- Type: integer, null
- Example: `10`

The actual limit

---

# Content Management API — Get all the subscription limits

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/subscription-limit/instances.md

## Returns

Returns an array of resource objects of type [subscription\_limit](/docs/content-management-api/resources/subscription-limit.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const subscriptionLimits = await client.subscriptionLimits.list();

  for (const subscriptionLimit of subscriptionLimits) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(subscriptionLimit);
  }
}

run();
```

Returned output

```javascript
{ id: "locales", code: "users", usage: 2, limit: 10 }
```

---

# Content Management API — Get a single subscription limit

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/subscription-limit/self.md

## Returns

Returns a resource object of type [subscription\_limit](/docs/content-management-api/resources/subscription-limit.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const subscriptionLimitId = "locales";

  const subscriptionLimit =
    await client.subscriptionLimits.find(subscriptionLimitId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(subscriptionLimit);
}

run();
```

Returned output

```javascript
{ id: "locales", code: "users", usage: 2, limit: 10 }
```

---

# Content Management API — Subscription feature

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/subscription-feature.md

## Object payload

**`id`**

- Type: string
- Example: `"locales"`

ID of feature

**`type`**

- Type: string

Must be exactly `"subscription_feature"`.

**`code`**

- Type: string
- Example: `"sso"`

The codename for the feature

**`enabled`**

- Type: boolean

Whether the feature is available on the current project

**`in_use`**

- Type: boolean

Whether the project is currently using the feature

---

# Content Management API — Get all the subscription features

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/subscription-feature/instances.md

## Returns

Returns an array of resource objects of type [subscription\_feature](/docs/content-management-api/resources/subscription-feature.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const subscriptionFeatures = await client.subscriptionFeatures.list();

  for (const subscriptionFeature of subscriptionFeatures) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(subscriptionFeature);
  }
}

run();
```

Returned output

```javascript
{ id: "locales", code: "sso", enabled: true }
```

---

# Content Management API — SSO Settings

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-settings.md

Represents the Single Sign-on settings of the current DatoCMS project

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID

**`type`**

- Type: string

Must be exactly `"sso_settings"`.

**`idp_saml_metadata_url`**

- Type: null, string
- Example: `"https://my-org.oktapreview.com/app/XXXX/sso/saml/metadata"`

URL of Identity Provider SAML Metadata endpoint

**`scim_base_url`**

- Type: string
- Example: `"https://sso.datocms.com/scim"`

DatoCMS SCIM base URL

**`saml_acs_url`**

- Type: string
- Example: `"https://sso.datocms.com/XXX/saml/consume"`

DatoCMS SAML ACS URL

**`sp_saml_metadata_url`**

- Type: string
- Example: `"https://sso.datocms.com/XXX/saml/metadata"`

DatoCMS SAML Metadata URL

**`sp_saml_base_url`**

- Type: string
- Example: `"https://sso.datocms.com/XXX/saml"`

DatoCMS SAML Base URL

**`saml_token`**

- Type: string
- Example: `"a2a24ae5fbb2d955b1b4fa73f2dd58"`

DatoCMS SAML Token

**`idp_saml_metadata_xml`**

- Type: null, string
- Example: `'<?xml version="1.0" encoding="UTF-8"?>...'`

Identity Provider SAML Metadata

**`scim_api_token`**

- Type: string
- Example: `"as3dasjh1234hj1"`

DatoCMS SCIM API Token

**`default_role`**

- Type: null, [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

The default role assigned to SSO users that do not belong to any SSO group

---

# Content Management API — Retrieve SSO Settings

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-settings/self.md

## Returns

Returns a resource object of type [sso\_settings](/docs/content-management-api/resources/sso-settings.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoSettings = await client.ssoSettings.find();

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoSettings);
}

run();
```

Returned output

```javascript
{
  id: "312",
  idp_saml_metadata_url: "https://my-org.oktapreview.com/app/XXXX/sso/saml/metadata",
  scim_base_url: "https://sso.datocms.com/scim",
  saml_acs_url: "https://sso.datocms.com/XXX/saml/consume",
  sp_saml_metadata_url: "https://sso.datocms.com/XXX/saml/metadata",
  sp_saml_base_url: "https://sso.datocms.com/XXX/saml",
  saml_token: "a2a24ae5fbb2d955b1b4fa73f2dd58",
  default_role: null,
}
```

---

# Content Management API — Generate SSO token

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-settings/generate_token.md

## Returns

Returns a resource object of type [sso\_token](/docs/content-management-api/resources/sso-token.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoSettings = await client.ssoSettings.generateToken();

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoSettings);
}

run();
```

Returned output

```javascript
{ id: "312", scim_api_token: "as3dasjh1234hj1" }
```

---

# Content Management API — Update SSO Settings

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-settings/update.md

## Body parameters

**`idp_saml_metadata_url`**

- Optional
- Type: null, string
- Example: `"https://my-org.oktapreview.com/app/XXXX/sso/saml/metadata"`

URL of Identity Provider SAML Metadata endpoint

**`idp_saml_metadata_xml`**

- Optional
- Type: null, string
- Example: `'<?xml version="1.0" encoding="UTF-8"?>...'`

Identity Provider SAML Metadata

**`default_role`**

- Optional
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

The default role assigned to SSO users that do not belong to any SSO group

## Returns

Returns a resource object of type [sso\_settings](/docs/content-management-api/resources/sso-settings.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoSettings = await client.ssoSettings.update({});

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoSettings);
}

run();
```

Returned output

```javascript
{
  id: "312",
  idp_saml_metadata_url: "https://my-org.oktapreview.com/app/XXXX/sso/saml/metadata",
  scim_base_url: "https://sso.datocms.com/scim",
  saml_acs_url: "https://sso.datocms.com/XXX/saml/consume",
  sp_saml_metadata_url: "https://sso.datocms.com/XXX/saml/metadata",
  sp_saml_base_url: "https://sso.datocms.com/XXX/saml",
  saml_token: "a2a24ae5fbb2d955b1b4fa73f2dd58",
  default_role: null,
}
```

---

# Content Management API — SSO User

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-user.md

A Single Sign-On user exists when a DatoCMS project is connected to an external Identity Provider. An SSO user will not use the standard login procedure but has to go through SAML authentication. It can also be linked to one or more IdP groups.

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of user

**`type`**

- Type: string

Must be exactly `"sso_user"`.

**`username`**

- Type: string
- Example: `"mark.smith@example.com"`

Email

**`external_id`**

- Type: string, null
- Example: `"Ja23ekjhsad"`

Identity provider ID. It returns `null` if the credentials you are using cannot manage the SSO users of the project.

**`is_active`**

- Type: boolean

Whether this user is active on the identity provider. De-activated users won't be able to login.

**`first_name`**

- Type: string, null
- Example: `"Mark"`

First name

**`last_name`**

- Type: string, null
- Example: `"Smith"`

Last name

**`meta.last_access`**

- Type: date-time, null
- Example: `"2018-03-25T21:50:24.914Z"`

Date of last reading/interaction. It returns `null` if the credentials you are using cannot manage the SSO users of the project.

**`groups`**

- Type: Array<[ResourceLinkage\<"sso_group"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_group.md)>

All the users's groups

**`role`**

- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md), null

The user role

---

# Content Management API — List all users

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-user/instances.md

## Returns

Returns an array of resource objects of type [sso\_user](/docs/content-management-api/resources/sso-user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoUsers = await client.ssoUsers.list();

  for (const ssoUser of ssoUsers) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(ssoUser);
  }
}

run();
```

Returned output

```javascript
{
  id: "312",
  username: "mark.smith@example.com",
  external_id: "Ja23ekjhsad",
  is_active: true,
  first_name: "Mark",
  last_name: "Smith",
  meta: { last_access: "2018-03-25T21:50:24.914Z" },
  groups: [{ type: "sso_group", id: "312" }],
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Returns a SSO user

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-user/self.md

## Returns

Returns a resource object of type [sso\_user](/docs/content-management-api/resources/sso-user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const userId = "312";

  const ssoUser = await client.ssoUsers.find(userId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoUser);
}

run();
```

Returned output

```javascript
{
  id: "312",
  username: "mark.smith@example.com",
  external_id: "Ja23ekjhsad",
  is_active: true,
  first_name: "Mark",
  last_name: "Smith",
  meta: { last_access: "2018-03-25T21:50:24.914Z" },
  groups: [{ type: "sso_group", id: "312" }],
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Copy editors as SSO users

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-user/copy_users.md

Copy existing users into SSO users

## Returns

Returns an array of resource objects of type [sso\_user](/docs/content-management-api/resources/sso-user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoUsers = await client.ssoUsers.copyUsers();

  for (const ssoUser of ssoUsers) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(ssoUser);
  }
}

run();
```

Returned output

```javascript
{
  id: "312",
  username: "mark.smith@example.com",
  external_id: "Ja23ekjhsad",
  is_active: true,
  first_name: "Mark",
  last_name: "Smith",
  meta: { last_access: "2018-03-25T21:50:24.914Z" },
  groups: [{ type: "sso_group", id: "312" }],
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — Delete a SSO user

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-user/destroy.md

## Query parameters

**`destination_user_type`**

- Type: enum
- Example: `"user"`

New owner for resources previously owned by the deleted SSO user. This argument specifies the new owner type.

<details>
<summary>Show enum values</summary>

**`account`**

**`user`**

**`access_token`**

**`sso_user`**

</details>

**`destination_user_id`**

- Type: string
- Example: `"7865"`

New owner for resources previously owned by the deleted SSO user. This argument specifies the new owner ID.

## Returns

Returns a resource object of type [sso\_user](/docs/content-management-api/resources/sso-user.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const userId = "312";

  const ssoUser = await client.ssoUsers.destroy(userId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoUser);
}

run();
```

Returned output

```javascript
{
  id: "312",
  username: "mark.smith@example.com",
  external_id: "Ja23ekjhsad",
  is_active: true,
  first_name: "Mark",
  last_name: "Smith",
  meta: { last_access: "2018-03-25T21:50:24.914Z" },
  groups: [{ type: "sso_group", id: "312" }],
  role: { type: "role", id: "34" },
}
```

---

# Content Management API — SSO Group

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-group.md

A Single Sign-On group exists when a DatoCMS project is connected to an Identity Provider. These groups can be used to link DatoCMS roles to the Identity Provider's groups.

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID of group

**`type`**

- Type: string

Must be exactly `"sso_group"`.

**`name`**

- Type: string
- Example: `"Admin"`

Name of the group

**`priority`**

- Type: integer
- Example: `1`

When an user belongs to multiple groups, the role associated to the group with the highest priority will be used

**`role`**

- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Sso Group's role

**`users`**

- Type: Array<[ResourceLinkage\<"sso_user"\>](https://www-draft.datocms.com/docs/content-management-api/resources/sso_user.md)>

Group members

---

# Content Management API — List all SSO groups

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-group/instances.md

## Returns

Returns an array of resource objects of type [sso\_group](/docs/content-management-api/resources/sso-group.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoGroups = await client.ssoGroups.list();

  for (const ssoGroup of ssoGroups) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(ssoGroup);
  }
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Admin",
  priority: 1,
  role: { type: "role", id: "34" },
  users: [{ type: "sso_user", id: "312" }],
}
```

---

# Content Management API — Sync SSO provider groups to DatoCMS roles

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-group/copy_roles.md

## Returns

Returns a resource object of type [sso\_group](/docs/content-management-api/resources/sso-group.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoGroupId = "312";

  const ssoGroup = await client.ssoGroups.copyRoles(ssoGroupId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoGroup);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Admin",
  priority: 1,
  role: { type: "role", id: "34" },
  users: [{ type: "sso_user", id: "312" }],
}
```

---

# Content Management API — Update a SSO group

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-group/update.md

## Body parameters

**`priority`**

- Required
- Type: integer
- Example: `1`

When an user belongs to multiple groups, the role associated to the group with the highest priority will be used

**`role`**

- Required
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

Sso Group's role

## Returns

Returns a resource object of type [sso\_group](/docs/content-management-api/resources/sso-group.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoGroupId = "312";

  const ssoGroup = await client.ssoGroups.update(ssoGroupId, {
    id: "312",
    priority: 1,
    role: { type: "role", id: "34" },
  });

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoGroup);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Admin",
  priority: 1,
  role: { type: "role", id: "34" },
  users: [{ type: "sso_user", id: "312" }],
}
```

---

# Content Management API — Delete a group

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/sso-group/destroy.md

## Returns

Returns a resource object of type [sso\_group](/docs/content-management-api/resources/sso-group.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const ssoGroupId = "312";

  const ssoGroup = await client.ssoGroups.destroy(ssoGroupId);

  // Check the 'Returned output' tab for the result ☝️
  console.log(ssoGroup);
}

run();
```

Returned output

```javascript
{
  id: "312",
  name: "Admin",
  priority: 1,
  role: { type: "role", id: "34" },
  users: [{ type: "sso_user", id: "312" }],
}
```

---

# Content Management API — White-label settings

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/white-label-settings.md

Represents the white-label settings of the current DatoCMS project

## Object payload

**`id`**

- Type: string
- Example: `"312"`

ID

**`type`**

- Type: string

Must be exactly `"white_label_settings"`.

**`custom_i18n_messages_template_url`**

- Type: null, string
- Example: `"https://my-app-messages.netlify.app/:locale/message.json"`

URL of custom I18n messages. The :locale placeholder represents the current DatoCMS UI locale.

---

# Content Management API — Retrieve white-label settings

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/white-label-settings/self.md

## Returns

Returns a resource object of type [white\_label\_settings](/docs/content-management-api/resources/white-label-settings.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const whiteLabelSettings = await client.whiteLabelSettings.find();

  // Check the 'Returned output' tab for the result ☝️
  console.log(whiteLabelSettings);
}

run();
```

Returned output

```javascript
{
  id: "312",
  custom_i18n_messages_template_url: "https://my-app-messages.netlify.app/:locale/message.json",
}
```

---

# Content Management API — Update white-label settings

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/white-label-settings/update.md

## Body parameters

**`custom_i18n_messages_template_url`**

- Optional
- Type: null, string
- Example: `"https://my-app-messages.netlify.app/:locale/message.json"`

URL of custom I18n messages. The :locale placeholder represents the current DatoCMS UI locale.

## Returns

Returns a resource object of type [white\_label\_settings](/docs/content-management-api/resources/white-label-settings.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const whiteLabelSettings = await client.whiteLabelSettings.update({});

  // Check the 'Returned output' tab for the result ☝️
  console.log(whiteLabelSettings);
}

run();
```

Returned output

```javascript
{
  id: "312",
  custom_i18n_messages_template_url: "https://my-app-messages.netlify.app/:locale/message.json",
}
```

---

# Content Management API — Audit log event

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/audit-log-event.md

If the Audit log functionality is enabled in a project, logged events can be queried using SQL-like language and fetched in full detail so that they can be exported or analyzed.

## Object payload

**`id`**

- Type: string
- Example: `"01F8WDQJR03M4VC6NTK49R83QW"`

ULID of event (https://github.com/ulid/spec)

**`type`**

- Type: string

Must be exactly `"audit_log_event"`.

**`action_name`**

- Type: string
- Example: `"items.publish"`

The actual action performed

**`actor`**

- Type: object
- Example: `{ type: "user", id: "3845289", name: "mark@acme.com" }`

The actor who performed the action

<details>
<summary>Show object format</summary>

**`type`**

- Type: string
- Example: `"user"`

The type of actor (can be `account`, `user`, `sso_user` or `access_token`)

**`id`**

- Type: string
- Example: `"3845289"`

The ID of the actor

**`name`**

- Type: string
- Example: `"mark@acme.com"`

An human representation of the actor (name/email/username depending on the type of actor)

</details>

**`role`**

- Type: null, object
- Example: `{ id: "455281", name: "Editor" }`

The role of the actor at the time the action was performed

<details>
<summary>Show object format</summary>

**`name`**

- Type: string
- Example: `"Editor"`

The name of the role

**`id`**

- Type: string
- Example: `"455281"`

The ID of the role

</details>

**`environment`**

- Type: object
- Example: `{ id: "main", primary: true }`

The environment inside of which the action was performed

<details>
<summary>Show object format</summary>

**`id`**

- Type: string
- Example: `"main"`

The ID of the environment

**`primary`**

- Type: boolean

Whether the environment was the primary one at the time the action was performed

</details>

**`request`**

- Type: object

The actual request being performed

Example:

```json
{
  id: "894f9f6c-a693-4f93-a3fb-452454b41313",
  method: "PUT",
  path: "/items/37823421/publish",
  payload: {},
}
```

<details>
<summary>Show object format</summary>

**`path`**

- Type: string
- Example: `"/items/37823421/publish"`

The full path of the request

**`method`**

- Type: string
- Example: `"PUT"`

The HTTP method of the request

**`id`**

- Type: string
- Example: `"894f9f6c-a693-4f93-a3fb-452454b41313"`

The X-Request-ID header of the request

**`payload`**

- Type: null, object
- Example: `{}`

The full HTTP body of the request

</details>

**`response`**

- Type: null, object
- Example: `{ status: 200, payload: {} }`

The actual response being returned by DatoCMS

<details>
<summary>Show object format</summary>

**`status`**

- Type: integer
- Example: `"200"`

The HTTP status code of the response

**`payload`**

- Type: object
- Example: `"PUT"`

The full HTTP body of the response

</details>

**`impersonated`**

- Type: null, boolean

Whether the action was performed during a debug (impersonation) session by DatoCMS staff

**`meta.occurred_at`**

- Type: date-time
- Example: `"2016-09-20T18:50:24.914Z"`

The date of the event

---

# Content Management API — List Audit Log events

Source [docs]: https://www.datocms.com/docs/content-management-api/resources/audit-log-event/query.md

The Audit Logs API allows to monitor events happening in an Enterprise project. It ensures continued compliance, safeguarding against any inappropriate system access, and allows you to audit suspicious behavior within your enterprise.

You can use this part of the API to:

-   Automatically feed DatoCMS access data into an SIEM or other auditing tool;
-   Proactively monitor for potential security issues or malicious access attempts;
-   Write custom apps to gain insight into how your organization uses DatoCMS.

Please note that DatoCMS does not perform any kind of automated intrusion detection. The Audit Logs API will return the data but can not automatically determine or indicate whether an action was appropriate.

### Pagination

A single request might not return the full results. To get the remaining results, you can use the `meta.next_token` of a response as a `next_token` attribute for the next request, until the response returns `null` as the next token.

### Filtering by date range

You can use the `since` and `before` parameters to restrict the events to a specific time range. Both accept an ISO 8601 datetime string.

For example, to return actions performed in Q1 2024 (January to March), set `since` to `"2024-01-01T00:00:00Z"` and `before` to `"2024-04-01T00:00:00Z"`.

### Filtering events

You can use the `filter` parameter to pass an SQL-like query ([PartiQL](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html)) to filter events. Any attribute of the event payload can be used in a condition.

```sql
-- Return actions of type 'items.update' only
action_name = 'items.update'

-- Returns actions whose name begins with 'fields'
begins_with(action_name, 'fields') -- Includes `fields.update`, `fields.destroy`, etc,

-- Returns actions containing 'destroy'
contains(action_name, 'update') -- Includes `fields.update`, `plugins.update`, `items.update`, etc.

-- Return actions performed by a collaborator
actor['type'] = 'user'

-- Return actions performed by a specific collaborator
actor['type'] = 'user' AND actor['id'] = '4845293'

-- Return publishing actions for the record 239408
request['path'] = '/items/239408/publish'

-- Return all record creations for the model 855832
action_name = 'items.create' AND request['payload']['data']['relationships']['item_type']['data']['id'] = '855832'
```

## Body parameters

**`since`**

- Optional
- Type: string
- Example: `"2024-01-01T00:00:00Z"`

Only return events occurred at or after this ISO 8601 datetime

**`before`**

- Optional
- Type: string
- Example: `"2024-04-01T00:00:00Z"`

Only return events occurred before this ISO 8601 datetime

**`filter`**

- Optional
- Type: string
- Example: `"action_name = 'items.update'"`

An SQL-like expression to filter the events

**`next_token`**

- Optional
- Type: string
- Example: `"E5188+SCXtvvXVUFkqmwtQJd3V3lJIOsZBjHvTYz"`

Set this value to get remaining results, if a meta.next_token was returned in the previous query response

**`detailed_log`**

- Optional
- Type: boolean

Whether a detailed log complete with full request and response payload must be returned or not

## Returns

Returns an array of resource objects of type [audit\_log\_event](/docs/content-management-api/resources/audit-log-event.md)

## Examples

###### Example Basic example

Code

```javascript
import { buildClient } from "@datocms/cma-client-node";

async function run() {
  const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

  const auditLogEvents = await client.auditLogEvents.query({});

  for (const auditLogEvent of auditLogEvents) {
    // Check the 'Returned output' tab for the result ☝️
    console.log(auditLogEvent);
  }
}

run();
```

Returned output

```javascript
{
  id: "01F8WDQJR03M4VC6NTK49R83QW",
  action_name: "items.publish",
  actor: { type: "user", id: "3845289", name: "mark@acme.com" },
  role: { id: "455281", name: "Editor" },
  environment: { id: "main", primary: true },
  request: {
    id: "894f9f6c-a693-4f93-a3fb-452454b41313",
    method: "PUT",
    path: "/items/37823421/publish",
    payload: {},
  },
  response: { status: 200, payload: {} },
  meta: { occurred_at: "2016-09-20T18:50:24.914Z" },
}
```

---

# Asset API — Images API

Source [docs]: https://www.datocms.com/docs/asset-api/images.md

Every asset you upload in DatoCMS is stored on [Imgix](https://www.imgix.com/), a super-fast CDN optimized for image delivery, which provides **on-the-fly image manipulations and caching**.

What that means is that simply by adding some parameters to your images URL, you can enhance, resize, crop, compress and change format for better performance. You can also create complex compositions and extract useful metadata.

At any time you can request your image in a new size — with a new crop or whatever transformation you might need — the asset is created for you and automatically cached close to your users. One thing to keep in mind as you implement your front-end is that to achieve maximum performance, **you should take care to reuse crops and sizes across your front-end** to ensure your cached assets are re-used.

Also, all new projects in are configured with an [automatic image optimization](/docs/asset-api/asset-cdn-settings.md) preset that selects the best format for compression without compromising the visual quality of your assets.

> [!NOTE] Should I use the Images API or the CDA with responsiveImage parameters?
> All your images on DatoCMS go through Imgix, but the same transformations and parameters can be accessed through two related APIs:
> 
> -   The Images API (this page) lets you apply image transformations directly via URL parameters, which is good for testing and directly fetching images.
>     
> -   But most production frontends use our GraphQL-based Content Delivery API (CDA) instead. And in that API, you can directly specify Imgix parameters right inside your query, and our GraphQL will automatically generate the correct Image API URL parameters for you. For more details, please see Content Delivery API: [Images and videos](/docs/content-delivery-api/images-and-videos.md)

### Powerful transformations at your disposal

(Image content)

The URL that DatoCMS will assign to the images you upload to your project will follow this structure:

```plaintext
https://<ASSET_DOMAIN>/<PROJECT_ID>/<UNIQUE_STAMP>-<ASSET_NAME>.<FORMAT>

Example:
-> https://www.datocms-assets.com/205/1570696780-example.jpg
```

If you fetch this URL, you will be served the original asset. This wastes a lot of bandwidth as content editors should upload full resolution assets. Thanks to Imgix, the DatoCMS image pipeline allows to scale, crop, and process images on the fly based on the URL-parameters you provide.

For example, by appending `?h=200` to the base URL, you instruct DatoCMS to scale the image to be 200 pixels tall:

```plaintext
https://www.datocms-assets.com/205/1570696780-example.jpg?h=200
```

But you can specify any number of parameters! Take a look at [Imgix's Image API Reference](https://docs.imgix.com/apis/url) page to see all the transformations available.

```plaintext
https://www.datocms-assets.com/205/1570542926-example.jpg?fit=facearea&faceindex=2&facepad=5&sat=-100&w=800&h=500&fm=png&txt=%C2%A9%20Matheus%20Ferrero&txt-align=bottom,center&txt-color=FFF&txt-size=15&txt-pad=20
```

In the example above, the parameters will:

-   crop the image to be 800x500px, centering around the second face it recognizes inside the picture;
-   desaturate the image;
    
-   add a copyright caption at the bottom;
-   transform the format to be a PNG.
    

> [!PROTIP] Pro tip: Caching of transformations
> The first time the image is called with these parameters, Imgix will cache and propagate the resulting image in all their geographically positioned CDN servers; subsequent calls with the same parameters will not need to reprocess the image.

### Focal points

When the same image is used in different contexts with different aspect ratios, the classic problem we might encounter is being able to crop it **while preserving the key parts**:

(Image content)

DatoCMS provides a complete set of [automatic controls on the crop](https://docs.imgix.com/apis/rendering/size/crop-mode), but unfortunately these detection methods are all automatic, so the result in some cases may not be exactly what we expect.

With focal points, you can now **ensure that the key part of your images doesn't get cut off or misaligned** across multiple image sizes and ratios, by explicitly specifying a focal point for the image.

The interface allows you to preview the result of the crop operation on different aspect ratios:

(Video content)

Note that **the focal point is not automatically applied at the CDN level**: if you construct an image URL manually with crop parameters, the focal point has no effect unless you explicitly add `fp-x`, `fp-y`, and `crop=focalpoint` to the URL yourself.

The automatic injection of these parameters happens only through the GraphQL Content Delivery API. When you use the `responsiveImage` or `url` fields with `fit: crop`, DatoCMS transparently adds the right Imgix parameters to center the crop on the focal point. The result looks like this:

(Image content)

To have an overview on the media area and its features, check out this video tutorial:

[

(Image content)

Images and Image Optimization

Play video »

](https://www.datocms.com/user-guides/media-management/images-and-image-optimization.md)

[

(Image content)

Working with the Media Manager in DatoCMS

Play video »

](https://youtu.be/OmRFyDhSXG4)

### Using the Images API with our GraphQL Content Delivery API

While this page covers how to use our images API directly (by appending URL parameters), you can also programmatically define these same parameters in a GraphQL query when you are using our Content Delivery API.

For details on that, please see: Content Delivery API: [Images and videos](/docs/content-delivery-api/images-and-videos.md) .

---

# Asset API — Video API

Source [docs]: https://www.datocms.com/docs/asset-api/videos.md

DatoCMS natively supports video encoding and streaming, thanks to the integration with [Mux](https://mux.com/), the fastest and most advanced cloud encoding platform for on-demand streaming video.

Every video you upload to your DatoCMS project will be instantly available for streaming. We can ingest almost every available codec, including those for broadcast and professional applications like H.264, H.265, VP9, and Apple ProRes.

Thanks to HLS Adaptive Bitrate (ABR) streaming, every viewer will always download the right video size for their device and connection speed from the nearest CDN node.

> [!NOTE] Prefer using YouTube streaming instead?
> No problem! We also support integrations with embedded videos from YouTube/Vimeo/Facebook as a special field type you can add to your models and blocks.

### Uploading videos

You can upload videos in the same way you upload regular assets. Through the interface, you can access some metadata related to the video, and you'll have the ability to preview it instantly:

(Image content)

You can add a video to your models using the *Single Asset* or *Asset Gallery* fields.

### What gets exposed via API

From your application, you can obtain everything you need to generate a video player through the API, as well as any thumbnails and other metadata. Take a look at the documentation of our [Content Delivery API](/docs/content-delivery-api/images-and-videos.md#videos) for all the details.

DatoCMS also offers `<VideoPlayer />` components for [React](https://github.com/datocms/react-datocms/blob/master/docs/video-player.md), [Vue](https://github.com/datocms/vue-datocms/tree/master/src/components/VideoPlayer), and [Svelte](https://github.com/datocms/datocms-svelte/tree/main/src/lib/components/VideoPlayer), making it easy to display a fully-featured video with captions, multiple audio tracks, and timeline hover previews using data retrieved from the API.

### Subtitles, closed captions, additional audio tracks

With every video you upload, you can make your content more accessible and reach a global audience with subtitles, closed captions, and extra audio tracks.

After you've uploaded a video, and it's been processed correctly (i.e. you see the thumbnail and can play the preview), head over to the "Additional audio tracks and subtitles" section to upload both alternate audio tracks (in M4A, MP3, or WAV format) and subtitles (in SRT or VTT format).

(Video content)

### Auto-generated captions

We offer the option to automatically generate closed captions for your video directly from its audio using speech recognition and machine learning. All you need to provide is the language of your video and the description to display in the player.

(Video content)

The transcription quality is usually pretty good, but since it's machine-generated, we recommend double-checking the results, particularly when used with suboptimal audio recordings.

If you want to make adjustments, you can download the generated subtitles in .vtt format by clicking on the icon next to the subtitles name, make your changes, and then re-upload the file.

(Image content)

> [!WARNING] Feature limited to recently added videos
> The option to automatically generate captions is only available for videos uploaded in the last 7 days.

### **Poster time**

By default, the still image used to represent a video — the thumbnail you see in listings, and the poster frame the player shows before playback starts — is taken from the very middle-point frame of the clip. That frame is often not the one you want.

With **poster time**, you can pick the exact moment that best represents the video by scrubbing to the frame you want and setting it as the poster directly in the interface.

(Image content)

Note that **the poster time is not automatically applied when you build a thumbnail URL manually**: if you construct a Mux thumbnail URL yourself, you'll get the middle-point frame unless you explicitly add the **time** parameter to the URL.

The automatic injection of this parameter happens through the GraphQL Content Delivery API. When you request the `thumbnailUrl` field, DatoCMS transparently adds the correct timestamp so the generated thumbnail lands in the frame you chose. For details, see Content Delivery API: [Images and videos](/docs/content-delivery-api/images-and-videos.md#videos).

### Stream videos in 4K

> [!NOTE] Available only on Enterprise plans
> As of today, 4K video streaming is only available upon request for Enterprise plans. If you want it enabled for your account, you'll need to [reach out to our team](https://www.datocms.com/support.md).

If you upload a video with a resolution that exceeds 1080p, and have the "4K Video Streaming" feature enabled on your plan, the video player will be able to serve higher resolution streaming for your viewers (2K/1440p or 4k/2160p). The video player selects the best video resolution based both on the density of the screen and the actual size of the player in the page, so it will only serve these higher resolutions when supported.

In case you want to limit this case, you can stop providing streaming for a video above a certain resolution by using a `max_resolution` query parameter to the regular Playback URL. This modifies the resolution options available for the player to select from:

```none
https://stream.mux.com/{PLAYBACK_ID}.m3u8?max_resolution=1080p
```

The `max_resolution` parameter can be set to `720p`, `1080p`, `1440p`, or `2160p`.

### Pricing and availability

Integration with Mux is offered across all DatoCMS packages, each incorporating a generous amount of encoding/streaming minutes into the cost.

If you're subscribed to a paid plan and exceed your quota, your website will not experience any service disruption. At the end of the month, we'll bill you for any additional usage.

If your plan has 4K video streaming enabled, it will have an additional cost - actual seconds of videos delivered in a resolution higher than 1080p will be charged with a 3x multiplier on DatoCMS due to the higher costs that Mux applies in this case ([read more](/docs/plans-pricing-and-billing/overcharges-on-api-and-bandwidth.md#4k-video-streaming)).

### What happens if you downgrade or cancel your subscription?

Videos will be kept for 60 days after the subscription ends. After that, we'll delete the videos. If you then change your mind and reactivate the project, you will need to re-upload the videos.

This behavior is particular to videos, as they can be very big and expensive to retain. This does not apply to other assets or data in general.

### Explore more!

To gain a comprehensive understanding of videos and video optimization in DatoCMS, take a look at this video tutorial:

[

(Image content)

Videos and Video Optimizations

Play video »

](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)

---

# Asset API — Asset CDN Settings

Source [docs]: https://www.datocms.com/docs/asset-api/asset-cdn-settings.md

## Accessing Asset CDN Settings

Advanced Asset Settings are located within your "Project settings", under the "Asset CDN settings" section. Here, administrators with the appropriate permissions can set default parameters that will apply to all assets of a project.

(Image content)

## Automatic Image Optimization

DatoCMS offers a set of customizable parameters that can significantly boost your project's performance by leveraging the robust image optimizations and transformations of Imgix, our image CDN partner.

We strongly recommend you read the following documentation thoroughly before implementing changes. For a more technical reference about how these parameters work, please see the [Imgix Rendering API documentation](https://docs.imgix.com/apis/rendering/overview).

> [!NOTE] How are automatic optimization settings applied?
> The default Automatic Image Optimization settings that you define here will be **combined** with any additional ones you explicitly specify in the URL. For instance, if you have your defaults set to `?auto=format&q=50` (via "Custom settings"), then:
> 
> -   Adding `?w=40` to the URL of an image will make the final parameters equal to `auto=format&q=50&w=40`. Even though the default parameters are not explicitly visible, they are still implicitly applied.
>     
> -   Applying `?auto=enhance` to the URL will behave as `?auto=enhance&q=50`, because the same parameter (`auto`) specified again at the URL level will override the previously set default.
>     
> 
> If you ever want to bypass the defaults, you can skip automatic optimization by using the URL parameter `?skip-default-optimizations=true` (or using the argument `skipDefaultOptimizations: true` in your CDA requests).
> 
> Therefore, `?auto=enhance&skip-default-optimizations=true` will simply behave as `?auto=enhance`, with no additional parameters (`q=50` will be skipped even though it was specified in the defaults). But please be careful when using the `skip-default-optimizations` parameter, as it could significantly increase your bandwidth costs.

### Option 1: DatoCMS presets (recommended)

This is the default setting for newly-created DatoCMS projects.

This presets applies the `auto=format` parameter to your images, and is our recommended approach for basic image optimization. This preset has been carefully selected to intelligently optimize images, selecting the best format for efficient compression without compromising visual quality.

[Learn more about what `auto=format` does in the Imgix documentation](https://docs.imgix.com/apis/rendering/automatic#format).

### Option 2: Custom settings

For more control, you can also choose to specify custom defaults for three Imgix settings: `auto`, quality (`q`), and color space (`cs`):

(Image content)

#### Automatic (`auto`) parameter

The [`auto` parameter](https://docs.imgix.com/apis/rendering/auto/auto) simplifies the optimization process automated across your image repository. It offers four distinct settings, which might also be combined:

`auto=compress`

-   Reduces image size through best-effort techniques, applying aggressive compression.
-   Serves images in AVIF format, with fallbacks to WebP or JPEG based on browser support.
    
-   Overrides `fm` parameter for non-animated assets when used with `auto=compress`.
    

`auto=enhance`

-   Improves image quality by enhancing highlights, midtones, and shadows across all RGB channels.
-   Gives images a vibrant appeareance, which is ideal for editorial, stock, and user-generated content.
    

`auto=true`

-   Automatically adjusts images by applying additional parameters, starting with `auto=enhance`.
-   If `crop=faces` is set, `auto=true` will triggers `auto=redeye` for red-eye removal.
    

`auto=format`

-   Determines the optimal image format through automatic content negotiation.
-   Attempts to serve images in AVIF, falling back to WebP, JPEG, or PNG based on browser support.
    
-   Can be combined with `auto=compress` and/or `fm` to customize fallback logic.
    

`auto=redeye`

-   Automatically removes red-eye from detected faces, enhancing image quality.
    

For more details, refer to the [imgix documentation on `auto` parameter](https://docs.imgix.com/apis/rendering/auto/auto).

#### Output Quality `q` parameter

The [`q` parameter](https://docs.imgix.com/apis/rendering/format/q) controls the output quality of lossy file formats like jpg, webp, avif, or jxr. Key points include:

-   Values range from 0 to 100, with 75 set as the default - higher values increase image file size.
-   Quality can often be set lower than default, especially for high-DPR (Device Pixel Ratio) images.
    
-   When auto=compress is applied, the default is automatically set to 45, unless overridden.
    

Explore more about the [q parameter in `imgix` documentation](https://docs.imgix.com/apis/rendering/format/q).

#### Color Space ( `cs` ) parameter

The [`cs` parameter](https://docs.imgix.com/apis/rendering/format/cs) specifies the color space of the output image. Options include:

-   **sRGB**: Default value, standard web color representation.
-   **Adobe RGB (1998)**: Provides accurate color reproduction from digital screens to print.
    
-   **TinysRGB**: Reduced color space metadata, potentially resulting in a slight color shift.
-   **Strip**: Removes color space for maximum size reduction.
    

Learn more about the [cs parameter in `imgix` documentation](https://docs.imgix.com/apis/rendering/format/cs).

## Option 3: No settings

No automatic image optimizations will be applied. This option is not typically recommended, because bypassing `auto=format` will result in significant bandwidth usage, especially if you're serving large .PNG files. Nonetheless, it can be useful if you prefer to use URL queries or GraphQL parameters to more precisely optimize your images on your frontend.

## Video optimization

### Block Serving Raw Videos

This is enabled by default for newly-created DatoCMS projects, and we recommend leaving it on unless you have a special use case for raw videos. By "raw", we mean that you are serving the video file (typically a .MP4, sometimes a .MOV or .AVI or other file type) directly from `datocms-assets.com`, without any optimizations for different connection speeds or devices. This can result in an inferior user experience.

Instead of serving the files directly, we generally recommend using HLS (HTTP Live Streaming) to serve videos to your visitors, because this improves performance and user experience for them, and minimizes bandwidth charges for you. Please see our documentation on [How to stream videos efficiently](/docs/streaming-videos/how-to-stream-videos-efficiently.md) for more information on how this works.

This setting allows project administrators to completely block raw video files from being served from your project, enforcing the use of HLS and Mux instead. This can help prevent human error (editors or developers accidentally linking to a raw .MP4) causing severe slowdowns for your visitors and excessive bandwidth use in your project.

Enabling this feature means that accessing a video's raw URL will result in a 422 error. This policy supports our standard best practice of [utilizing Mux for video delivery](/docs/asset-api/videos.md), optimizing video streaming and ensuring consistency across the platform.

[

(Image content)

Images and Image Optimization

Play video »

](https://www.datocms.com/user-guides/media-management/images-and-image-optimization.md)

[

(Image content)

Videos and Video Optimizations

Play video »

](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)

---

# Real-time Updates API — Real-Time Updates API Overview

Source [docs]: https://www.datocms.com/docs/real-time-updates-api.md

The Real-time Updates API allows clients to **listen for content changes using a stable connection that streams events as they occur**. It supports the exact same GraphQL queries available in the [Content Delivery API](/docs/content-delivery-api.md), but returns a streaming channel implementing the [Server-Sent Events protocol](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events), which is natively supported by modern browsers.

### Use cases

Live updates can be **extremely useful for content editors** to preview draft content on the real website as it gets authored, without needing a page refresh or additional staging servers:

(Video content)

Live updates **can also be pushed to regular visitors**, so that thay can immediately see new content as it gets published by editors, allowing all kinds of real-time interactions with your website/app.

Imagine a real-time updated event coverage liveblog, for example:

(Video content)

### Examples and API reference

We recommend using our [client libraries](/docs/real-time-updates-api/listening-to-queries.md) to listen for updates, as they will set-up the streaming channel for you.

If you need to implement the streaming logic on other environments than the browser, then read the [low-level reference](/docs/real-time-updates-api/api-reference.md) of the underlying endpoints.

---

# Real-time Updates API — How to use it

Source [docs]: https://www.datocms.com/docs/real-time-updates-api/listening-to-queries.md

If you want to use real-time updates on the browser, the easiest way is to use one of our libraries. They will handle all the hard-wiring for you, including reconnecting to a new subscription channel in case of network errors.

### Next.js

Please take a look at our [Next.js integration guide](/docs/next-js/real-time-updates.md) to learn how you can use the Real-time Updates API to produce instant refresh of content as soon as it gets saved into DatoCMS.

We have also prepared a [step-by-step tutorial](https://www.datocms.com/blog/live-preview-with-next-js.md) that shows how to get to live-previews of draft content, so be sure to check that out!

You can also deploy and play with the code of one of our Next.js project starters, as they both support real-time updates:

[

(Image content)

Next.js Starter Kit

Try this demo »

](https://www.datocms.com/marketplace/starters/next-js-starter-kit.md)

### React

If you're in a React project the [`react-datocms`](https://github.com/datocms/react-datocms#live-real-time-updates) package exposes a `useQuerySubscription` hook that makes it trivial to make any webpage updated in real-time.

For more info on all the available options, please refer to its [documentation on Github](https://github.com/datocms/react-datocms#live-real-time-updates):

```jsx
import React from "react";
import { useQuerySubscription } from "react-datocms";

const App: React.FC = () => {
  const { status, error, data } = useQuerySubscription({
    query: `
      query AppQuery($first: IntType) {
        allBlogPosts {
          slug
          title
        }
      }`,
    variables: { first: 10 },
    token: "YOUR_API_TOKEN",
  });

  const statusMessage = {
    connecting: "Connecting to DatoCMS...",
    connected: "Connected to DatoCMS, receiving live updates!",
    closed: "Connection closed",
  };

  return (
    <div>
      <p>Connection status: {statusMessage[status]}</p>
      {error && (
        <div>
          <h1>Error: {error.code}</h1>
          <div>{error.message}</div>
          {error.response && (
            <pre>{JSON.stringify(error.response, null, 2)}</pre>
          )}
        </div>
      )}
      {data && (
        <ul>
          {data.allBlogPosts.map((blogPost) => (
            <li key={blogPost.slug}>{blogPost.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
};
```

### Vanilla JS

On any other JS environment you can use the [`datocms-listen`](https://github.com/datocms/datocms-listen) package which exposes a generic `subscribeToQuery` function that encapsulates all the connection logic.

For more info on all the available options, please refer to its [documentation on Github](https://github.com/datocms/datocms-listen):

```javascript
import { subscribeToQuery } from "datocms-listen";

const unsubscribe = await subscribeToQuery({
  query: `
    query BlogPosts($first: IntType!) {
      allBlogPosts(first: $first) {
        title
        nonExistingField
      }
    }
  `,
  variables: { first: 10 },
  token: "YOUR_TOKEN",
  includeDrafts: true,
  onUpdate: (response) => {
    // response is the GraphQL response
    console.log(update.response.data);
  },
  onStatusChange: (status) => {
    // status can be "connected", "connecting" or "closed"
    console.log(status);
  },
  onChannelError: (error) => {
    // error will be something like:
    // {
    //   code: "INVALID_QUERY",
    //   message: "The query returned an erroneous response. Please consult the response details to understand the cause.",
    //   response: {
    //     errors: [
    //       {
    //         fields: ["query", "allBlogPosts", "nonExistingField"],
    //         locations: [{ column: 67, line: 1 }],
    //         message: "Field 'nonExistingField' doesn't exist on type 'BlogPostRecord'",
    //       },
    //     ],
    //   },
    // }
    console.error(error);
  },
});
```

---

# Real-time Updates API — API reference

Source [docs]: https://www.datocms.com/docs/real-time-updates-api/api-reference.md

The Real-time Content API is built upon and extends the capabilities of the GraphQL [Content Delivery API](/docs/content-delivery-api.md): theyboth support exactly the same [authentication method](/docs/content-delivery-api/authentication.md), [endpoints](/docs/content-delivery-api/api-endpoints.md) and [GraphQL queries](/docs/content-delivery-api/how-to-fetch-records.md).

What's different is the domain you use to perform the POST request:

-   Content Delivery API: `https://graphql.datocms.com`
-   **Real-time Content API:** `https://graphql-listen.datocms.com`
    

And of course the response you'll receive:

-   a call to the Content Delivery API simply returns a JSON with the response to the requested query, whereas
-   the same call to the Real-time Content API **returns the URL of a persistent channel** implementing the [Server-Sent Events protocol](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).
    

Here's a diagram representing the differences between the two:

(Image content)

In the following video, you can see how how easy it is to interact with the API simply using `curl`:

(Video content)

## Obtaining the URL of the channel

Using the same headers, API token and endpoint you use with the [Content Delivery API](/docs/content-delivery-api.md), you can perform a request to `https://graphql-listen.datocms.com` and get back the URL of a Server-Sent Events channel that streams events as they occur:

Terminal window

```bash


# Content Delivery API call

~() curl https://graphql.datocms.com/ \
        -H 'Authorization: Bearer YOUR_TOKEN' \
        -H 'X-Include-Drafts: true' \
        -d '{"query":"{ blogPost{ id } }"}'

{ "data": { "blogPost": { "id": "9721019" } } }

# Real-time Content API call

~() curl https://graphql-listen.datocms.com/preview \
        -H 'Authorization: Bearer YOUR_TOKEN' \
        -d '{"query":"{ blogPost{ id } }"}'

{ "url": "https://graphql-listen.datocms.com/channels/e4dae1a0-146e-4956-90e6-076ca9123eeb" }
```

The channel URL is ephemeral: after 15 seconds you will no longer be able to access it, so be sure to connect to it within a few seconds, or you will need to make a new call to get a new URL.

## Receiving the events

Once you obtain the URL of a subscription channel, you can connect to it and stream events as they occur.

All modern browsers offer a native interface to connect to Server-Sent Events channels called `EventSource`:

```javascript
const eventSource = new EventSource(
  "https://graphql-listen.datocms.com/channels/e4dae1a0-146e-4956-90e6-076ca9123eeb"
);

eventSource.addEventListener("open", () => {
  console.log("connected to channel!");
});
```

Immediately after the connection, the channel will send an `update` event with the result of the GraphQL query. The same event will then be sent every time the result of the query changes due to a change of the underlying content:

```javascript
eventSource.addEventListener("update", (event) => {
  const result = JSON.parse(event.data);

  // result will be something like:  { data: { blogPost: { id: "9721019" } } }
  console.log("updated graphql result: ", result);
});
```

When something goes wrong, the channel can also send `channelError` events. The cause for the error could be an invalid GraphQL query for example:

```javascript
eventSource.addEventListener("channelError", (event) => {
  const error = JSON.parse(event.data);

  // error will be something like:
  // {
  //   code: "INVALID_QUERY",
  //   message: "The query returned an erroneous response. Please consult the response details to understand the cause.",
  //   fatal: true,
  //   response: {
  //     errors: [
  //       {
  //         fields: ["query", "blogPost", "coverImage", "url", "wrongParameter"],
  //         locations: [{ column: 99, line: 1 }],
  //         message: "Field 'url' doesn't accept argument 'wrongParameter'",
  //       },
  //     ],
  //   },
  // }

  if (error.fatal) {
    eventSource.close();
  }
});
```

The `fatal` field in the error object is useful to know if the error is temporary (ie. connectivity/network problems) and therefore it is possible that additional `update`\-type events are sent, or if the error is fatal (ie. invalid query) so you need to close the channel permanently.

## Closed channels

An SSE channel stays open as long as possible, but it is perfectly normal that it closes after some time. The channel closure may be due to an automatic re-scaling of our servers to cope with an increase in requests, for example.

The official clients we have released handle this case transparently, and reconnect automatically if the channel closes. If you don't use one of our libraries, you'll have to make sure to reopen a new channel yourself in case the event happens.

---

# Real-time Updates API — Real-time Updates API Limits & Pricing

Source [docs]: https://www.datocms.com/docs/real-time-updates-api/limits-and-pricing.md

The Real-time Updates API is capable of supporting hundreds of thousands of concurrent connections.

### Technical Limits

Every plan comes with a **technical limit of a maximum of 500 concurrent connections per project**. This means, at the same time, that there can be a maximum of 500 open SSE connections to the same project. If you need more, please [contact us](https://www.datocms.com/support.md?topics=technical-support/general-request) so we can discuss your needs and see how we can help you scale.

### Pricing

Even though the Real-time Updates API is not billed per se, it uses the [Content Delivery API](/docs/content-delivery-api.md) to function, so it contributes to the number of API calls to it.

What makes the cost predictable is that **CDA usage is independent of the number of connected clients**. Clients that open an identical subscription request are served by a single shared re-fetch. Whether 1 or 500 clients are watching, a content change triggers the same single CDA request, and every client receives the update.  

A few consequences follow from this model:

-   **One request per relevant change.** Each content change that affects a query's result triggers exactly one CDA request for that query.
-   **Irrelevant changes are free.** Edits elsewhere in the project that don't affect a query's result trigger no re-fetch, and therefore no CDA request, for that query.
    
-   **Usage scales with distinct subscriptions, not clients.** Subscriptions are grouped together only when their requests match exactly; any difference — in the query, its variables, or any other request detail — makes them count separately, each with its own re-fetch. Your CDA usage therefore grows with the number of *distinct* subscriptions being watched, not with the number of connected clients.
    

#### Example

Imagine 500 visitors viewing the same homepage, all subscribed to the same hardcoded query, with content changing every 30 seconds.

Because every visitor shares the same query, they are served by a single re-fetch. Each change produces one CDA request, so a change every 30 seconds results in **2 CDA requests per minute in total — regardless of the 500 connected clients**.

By contrast, if those same 500 visitors each subscribed to a *different* query, a single homepage change could trigger up to 500 re-fetches — one per unique query — because there is no longer anything to share.

---

# AI & Automation at a glance — AI and Automation

Source [docs]: https://www.datocms.com/docs/ai-overview/overview-of-ai-and-automation.md

DatoCMS offers several approaches to help you automate your work when managing content with LLMs.

## Agent Skills

DatoCMS Skills are playbooks that AI coding assistants load on demand. Without them, your LLM can call DatoCMS APIs, but might take a guess at conventions, pick wrong field types, and miss integration patterns. With DatoCMS's agent skills, agents work with correct GraphQL queries, solid content modeling, idiomatic frontend integrations, clean migrations, and well-structured plugins, all on the first try.

Agent skills are currently available to help you build with DatoCMS (Content Modeling, Reading Content, Writing Content, CLI Workflows, and handling Frontend Integrations), and to extend DatoCMS's capabilities with helping you develop Plugins (Plugin Scaffolding, maintenance, and managing the design system).

Get started with DatoCMS [Agent Skills here](/docs/agent-skills.md),

Or [check out the repo here](https://github.com/datocms/agent-skills).

## DatoCMS MCP Server

The DatoCMS MCP server connects DatoCMS directly to AI assistants through the Model Context Protocol (MCP). It allows MCP-compatible tools, such as Claude Code, Claude Desktop, ChatGPT, Cursor, VS Code, and Windsurf, to interact with your DatoCMS projects using natural language commands.

Our recommendation is to use the DatoCMS MCP to help your content and editor teams to improve their workflows, and to use Agent Skills when interacting with the DatoCMS APIs.

[Get started with the MCP Server here](/docs/mcp-server.md),

or [share this User Guide with your editorial team](https://www.datocms.com/user-guides/content-management/working-with-the-datocms-mcp.md) to help them unlock new workflows.

## AI powered translations

DatoCMS's [(Image content)AI Translations](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md)plugin integrates leading AI providers—OpenAI (ChatGPT), Google (Gemini), Anthropic (Claude), and DeepL — directly into DatoCMS. The plugin helps you translate individual fields, entire records, multiple records from table views, or batch-translate whole models.

The plugin intelligently handles complex field types including Structured Text, supports contextual translations for better accuracy, and preserves ICU Message Format patterns, allowing you to achieve things like preserving complex formatting, using the right context, and enforcing terminology.

Learn more about what you can do [with this plugin here](/docs/translating-content-with-ai.md).

## LLM-ready Docs

All our API references and docs (and most of our website) are LLM-ready. There's 3 approaches you can access to provide the best possible context for your LLMs when working with DatoCMS:

-   Complete Docs: [https://www.datocms.com/docs/llms-full.txt](https://www.datocms.com/docs/llms-full.txt) is where you will find a complete `.md` of all our docs, to give your LLM a quick reference on all topics and guides.
-   Documentation Index: [https://www.datocms.com/docs/llms.txt](https://www.datocms.com/docs/llms.txt) is where you can provide your LLMs with an index of all docs pages to let it decide which path would best serve your use-case at hand.
    
-   Single-page `.md` exports: All DatoCMS website pages are available as a markdown file by appending `.md` to the end of the URL to give your LLM a quick extraction of specific pages without dealing with HTML formatting issues or web scraping complications. On content pages like docs and blog, you can simply select "copy page" as markdown as well.
    

Learn more about the [application and usage for this here](/docs/llm-ready-docs.md).

---

# Agent Skills — Agent Skills

Source [docs]: https://www.datocms.com/docs/agent-skills.md

**DatoCMS Skills** are expert playbooks that AI coding assistants load on demand. Without them, an AI can call DatoCMS APIs, but will guess at conventions, pick wrong field types, and miss integration patterns. With them, **agents work like a senior DatoCMS developer**: correct GraphQL queries, solid content modeling, idiomatic frontend integrations, clean migrations, and well-structured plugins — on the first try.

> [!PROTIP] Pro tip: Skills or MCP?
> -   **Working in a local repo?** Install Agent Skills — **they're all you need**. Every MCP capability is already built in via local CLI calls, plus all the expertise MCP can't carry: content modeling best practices, frontend integration patterns, migration workflows, plugin conventions, and more.
>     
> -   **No local terminal?** (web, mobile, chat apps) Use the [**MCP server**](/docs/mcp-server.md) instead.
>     
> 
> Pick one, not both.

## What you can do

Most skills trigger automatically based on your prompt.

###### Building with DatoCMS

-   **Content modeling** — schema-design decisions: model vs block, references vs embedded blocks, taxonomies, field shapes, validators, editor appearances.
-   **Reading content** — GraphQL queries against the Content Delivery API: filters, pagination, localization, modular content, Structured Text, responsive images, SEO metadata, typed queries with gql.tada or codegen.
    
-   **Writing content & automation** — programmatic CMA scripts: record CRUD, bulk imports/exports, asset uploads, environment forks and promotions, webhooks, roles and tokens, scheduled publishing, audit logs.
-   **CLI workflows** — migrations, schema-type generation, typed CMA scripts, environment operations, CI/CD pipelines, WordPress/Contentful imports.
    
-   **Frontend integrations** — draft mode, Web Previews, Visual Editing, Content Link overlays, real-time preview subscriptions, cache-tag invalidation, SEO/sitemap wiring across Next.js App Router, Nuxt, SvelteKit, Astro, plus `react-datocms`, `vue-datocms`, `@datocms/svelte`, and `@datocms/astro`.
    

###### Building DatoCMS plugins

-   **Plugin scaffolding** — create a brand-new plugin from scratch with the Vite/React structure, picking the initial surfaces (field extensions, config screens, sidebars, pages, asset sources).
-   **Plugin maintenance** — patch and extend an existing plugin: hook additions, field extension tweaks, sidebar/page changes, validation updates.
    
-   **Plugin design system** — restyle plugin UI to feel native to the DatoCMS dashboard — config screens, panels, modals, forms, density and spacing.
    

## Installation

Pick the install method for your agent. Every installer brings the full set by default — the skills are cross-linked and meant to work together.

Claude Code

Claude Code's native plugin system bundles all DatoCMS skills with auto-update support and namespaced invocation:

Terminal window

```bash
/plugin marketplace add datocms/agent-skills
/plugin install datocms@datocms-skills
```

All skills install; the right one activates based on your prompt. Auto-update from `/plugin` → **Marketplaces** → `datocms-skills`.

Claude.ai (web)

CLI commands and local file editing aren't available on the web, so most development-focused skills won't apply. The two worth uploading are:

-   [`datocms-content-modeling.zip`](https://github.com/datocms/agent-skills/raw/master/zips/datocms-content-modeling.zip) (content modeling)
-   [`datocms-cma.zip`](https://github.com/datocms/agent-skills/raw/master/zips/datocms-cma.zip) (writing content & automation)
    

To add them, go to **Customize → Skills** → **+** → **Upload a skill**,and drop both .zip files.

**These work best alongside the** [**DatoCMS MCP server**](/docs/mcp-server.md), which lets the agent read and update your project directly from the conversation.

Codex app and CLI

Codex also supports installing through its plugin marketplace. Add the DatoCMS marketplace once, then install the plugin from the Codex plugin picker inside a session:

Terminal window

```bash
codex plugin marketplace add datocms/agent-skills
```

Then open a Codex session and install from the plugin picker:

Terminal window

```bash
/plugins
```

Choose **DatoCMS** and "Install plugin"

Other agents

The universal `npx skills` installer brings the full set:

Terminal window

```bash
npx skills add datocms/agent-skills --skill '*'
```

Code Editors

To use Agent Skills in VS Code, Cursor, Windsurf, install first the [Claude Code extension](https://marketplace.visualstudio.com/items?itemName=anthropic.claude-code) or the [Codex extension](https://marketplace.visualstudio.com/items?itemName=openai.chatgpt)

**Codex Extension, Cursor, Zed, and other editors**

Run this in the editor's integrated terminal from your project root:

Terminal window

```bash
npx skills add datocms/agent-skills --skill '*' --agent '*'
```

**Claude Code extension**

Run these commands in the Claude Code prompt, not the terminal:

Terminal window

```bash
/plugin marketplace add datocms/agent-skills
/plugin install datocms@datocms-skills
```

In Zed, install Codex or Claude Agent from the ACP Registry, then use the matching instructions above.

## Usage

###### Automatic skills

Most skills activate automatically. Describe your task in plain language and the right skill picks it up:

-   "Should testimonials be a model or a block?"
-   "Write a GraphQL query to fetch all blog posts with images"
    
-   "How do I paginate past the 100-record limit?"
-   "Add draft mode to my Next.js app"
    
-   "Why isn't my Visual Editing overlay showing up?"
-   "Create a migration that adds a `category` field to the blog\_post model"
    
-   "Bulk-publish all draft records of type `article`"
-   "Import this CSV into the authors model"
    
-   "Make my plugin config screen match the DatoCMS style"
-   "Create a new DatoCMS plugin from scratch"
    

###### The setup skill (explicit)

`datocms-setup` is the one skill you invoke explicitly. It handles end-to-end project bootstrapping — draft mode, visual editing, migrations workflows, content imports — and queues prerequisites automatically when needed.

Phrase prompts as the outcome you want:

Claude Code

```text
/datocms-setup install visual editing in this project
/datocms-setup set up draft mode and web previews
/datocms-setup add migrations and a release workflow
```

Codex

```text
$datocms-setup install visual editing in this project
$datocms-setup set up draft mode and web previews
$datocms-setup add migrations and a release workflow
```

## How it works

Unlike a raw API integration, Skills package not just *what* DatoCMS exposes but *how* to use it well — so the agent gets your task right on the first try.

###### Description-based routing

Each skill ships with a description. When you describe a task, the agent inspects all installed skill descriptions and loads the most relevant one into context. You don't call skills explicitly — they activate themselves.

###### Local files, your control

Skills are markdown plus a few helper assets, stored on disk in `.agents/skills/` (universal) or per-agent dirs like `~/.claude/skills/` and `~/.codex/skills/`. They're inspectable, version-controllable, and removable at any time.

###### Cross-skill references

Skills hand off to each other. The setup skill might queue a migration via the CLI skill, which in turn types its TypeScript helpers via the CMA skill. Installing a single skill in isolation breaks these handoffs — that's why every installer brings the full set by default.

## Security

Skills run with the **full permissions of the host agent**. They can read and write files, run shell commands, and make network requests on your behalf. That power is useful, but it's also worth knowing.

-   All DatoCMS skills are open source and visible at [github.com/datocms/agent-skills](https://github.com/datocms/agent-skills). Inspect the markdown before granting an agent access to a sensitive repo.
-   The `npx skills` installer surfaces automated security scans (Gen / Socket / Snyk) alongside each skill at install time.

---

# MCP Server — MCP Server

Source [docs]: https://www.datocms.com/docs/mcp-server.md

The **DatoCMS MCP server** connects DatoCMS directly to AI assistants through the Model Context Protocol (MCP). It allows MCP-compatible tools, such as Claude Code, Claude Desktop, ChatGPT, Cursor, VS Code, and Windsurf, to interact with your DatoCMS projects using natural language commands.

> [!WARNING] Already coding locally? Use Agent Skills instead!
> If you already have a repo open in your editor or CLI, skip the MCP server and install [DatoCMS Agent Skills](/docs/agent-skills.md) instead. They automatically install the DatoCMS CLI, which talks directly to the DatoCMS APIs — faster and no MCP server needed. You get everything MCP can do, plus content modeling, frontend integrations, migrations, and plugin expertise.
> 
> **MCP is the right choice only where there's no local terminal to work in!**

## What you can do

The DatoCMS MCP server allows AI assistants to:

-   **Find your projects**: Search across every project you have access to (personal account and organizations) and operate on any of them within the same session, without restarting the server or swapping environment variables
-   **Inspect your schema**: Retrieve detailed information about your content models, fields, and relationships
    
-   **Execute API operations**: Perform both read-only and destructive operations on your DatoCMS project
    

(Video content)

## Requirements

-   An MCP-compatible client: **Claude Code** (recommended), Claude Desktop, ChatGPT, Cursor, VS Code, Windsurf, or other MCP clients
    

There is nothing to install locally. The server is hosted at `https://mcp.datocms.com` and authentication happens entirely in your browser through standard OAuth.

## Installation

The DatoCMS MCP server uses native MCP OAuth, so configuration only requires the server URL — no API tokens, no environment variables. Choose the installation method for your client:

Claude Code

Use the Claude Code CLI to add the DatoCMS MCP server. Claude Code will open a browser window so you can authorize access through OAuth.

Terminal window

```bash
claude mcp add --transport http DatoCMS https://mcp.datocms.com
```

Codex

Use the Codex CLI to add the DatoCMS MCP server. Codex will open a browser window so you can authorize access through OAuth.

Terminal window

```bash
codex mcp add DatoCMS --url https://mcp.datocms.com
```

Cursor

Go to **Cursor Settings** → **Tools & Integrations** → **New MCP Server**, then paste:

```json
{
  "mcpServers": {
    "DatoCMS": {
      "type": "http",
      "url": "https://mcp.datocms.com"
    }
  }
}
```

VS Code

Use the VS Code CLI:

Terminal window

```bash
code --add-mcp '{"name":"DatoCMS","type":"http","url":"https://mcp.datocms.com"}'
```

Or follow the MCP installation [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server) and use the same configuration in your `mcp.json`.

Claude Desktop / Claude.ai

Claude Desktop and Claude.ai both support remote MCP servers as **custom Connectors**:

1.  Go to **Customize → Connectors**
    
2.  Click the **+** button next to Connectors and select **Add custom connector**
    
3.  Enter a name (e.g. `DatoCMS`) and the URL `https://mcp.datocms.com/`
    
4.  Click **Add**
    

(Image content)

Once done, click **Connect** so you can authorize access through DatoCMS OAuth.

ChatGPT

ChatGPT supports remote MCP servers through **Developer Mode** (currently in beta).

1.  Enable Developer Mode at **Settings → Apps → Advanced settings → Developer mode**
    
2.  Open **Apps settings** and click **Create app** next to **Advanced settings**
    
3.  Enter a name (e.g. `DatoCMS`), the **MCP Server URL** `https://mcp.datocms.com`, and leave **Authentication** set to **OAuth**
    
4.  Click **Create**
    

(Image content)

Once done, click **Connect** so you can authorize access through DatoCMS OAuth. The new app will appear in the composer's **Developer Mode** tool during conversations.

Google Antigravity

In **Antigravity Settings → Customizations → Installed MCP Servers**, click the **Open MCP Config** button (or directly open `~/.gemini/antigravity/mcp_config.json`):

```json
{
  "mcpServers": {
    "DatoCMS": {
      "type": "http",
      "url": "https://mcp.datocms.com"
    }
  }
}
```

Windsurf

Add to `~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "DatoCMS": {
      "serverUrl": "https://mcp.datocms.com"
    }
  }
}
```

## How it works

Unlike traditional MCPs that expose every API endpoint, the DatoCMS MCP server takes a different approach designed to guide AI assistants through API interactions effectively.

###### Layered tools, not raw endpoints

DatoCMS has 40+ resources and 150+ API endpoints. Exposing all of them would overwhelm any LLM. Instead, we provide a small set of carefully designed tools organized in three layers (discovery → planning → execution) that guide the AI through a natural workflow:

**Discovery & planning** — Tools that explore what's available and load the documentation needed for execution:

-   `search_projects`: Search across every project in your account and organizations with fuzzy matching
-   `list_api_resources`: List all available DatoCMS API resources grouped by theme
    
-   `get_api_methods`: Batch-document any combination of resources, actions, and methods. Returns full TypeScript definitions and examples, and mints the verification tokens that the execution layer requires
-   `get_schema`: Retrieve detailed information about your content models, fields, relationships, and nested blocks
    

**Execution layer (read-only)** — These tools use a read-only API token. Most clients allow them to run without confirmation:

-   `upsert_and_execute_safe_script`: Write or patch a TypeScript script and execute it against a read-only client
-   `view_script`: View a previously stored script
    

**Execution layer (read-write)** — These tools use a full read-write API token. The user is asked to confirm before each use:

-   `upsert_and_execute_unsafe_script`: Write or patch a TypeScript script and execute it with full create/update/delete permissions
    

This layered structure reduces malformed API calls and forces the AI to commit to method names *before* writing code — every API call in a script must reference a verification token returned by `get_api_methods`.

###### Documentation-aware

Each documentation tool retrieves detailed method definitions and concrete examples from the official DatoCMS documentation. This is token-intensive, but it significantly improves success rates by giving the LLM the context it needs to make correct API calls.

###### Script-based execution

Instead of making one API call at a time, the DatoCMS MCP server enables AI assistants to write complete TypeScript programs that batch multiple operations to reduce round-trips and token overhead, provide full context for complex multi-step operations, support incremental editing when errors occur, and get type-checked before execution to catch errors early. Each script runs in an isolated runner on DatoCMS infrastructure, with restricted network access and a configurable timeout.

## Performance & reliability

###### Real-world performance

When using **Claude Code** (our recommended client), users report:

-   **Fast execution**: Most common operations complete quickly without noticeable delays
-   **High success rates**: Operations like creating records, adding images, managing translations, and linking records work reliably
    
-   **Minimal iteration needed**: Most tasks succeed on the first attempt, with occasional minor refinements needed through follow-up prompts
-   **No missteps**: The layered approach effectively prevents malformed API calls
    

###### What to expect

**Common operations (fast & reliable):**

-   Creating records with content across multiple fields
-   Uploading and attaching images to records
    
-   Adding translations to existing content
-   Linking records together
    
-   Copying content between models
-   Listing and querying records
    

**Complex operations (slower but functional):**

-   Very complex operations like generating complete landing pages may take several minutes
-   Large batch operations involving many records
    
-   Schema modifications across multiple models
    

###### Token consumption

The documentation-aware approach retrieves full method documentation and examples for each operation. This consumes more tokens than traditional MCPs but significantly improves success rates. For most practical tasks, the token cost is acceptable given the reliability and accuracy of results.

### Success tips

Results improve with:

-   **Clear, specific prompts**: "Create a blog post with title 'Hello' and attach the uploaded image" works better than "add a post"
-   **Iterative refinement**: If something is missing (like an image), a simple follow-up request handles it quickly
    

### When it works best

The DatoCMS MCP server excels at:

-   Everyday content management tasks (creating, updating, translating records)
-   Complex multi-step operations that require understanding your content model
    
-   Tasks that would be tedious to do manually (bulk updates, content migration)
-   Operations that benefit from type safety and validation
    

## Limits

Scripts that the AI executes on our runners are subject to the following limits. **These values apply during the initial beta phase and may change as we gather usage data:**

| Limit | Free plans | Paid plans |
| --- | --- | --- |
| Maximum execution time per script | 20 seconds | 60 seconds |
| Monthly script execution time per account | 50 minutes | 400 minutes |
| Maximum output captured from a single script | 32 KB | 32 KB |

A few notes on what these mean in practice:

-   **Per-script timeout**: if a script takes longer than the limit it is terminated and the AI receives a friendly error. For long-running operations (large migrations, full-site translations) ask the assistant to split the work into smaller scripts.
-   **Monthly time budget**: every second a script spends running counts against the account's monthly budget. The budget resets at the start of each calendar month (UTC). Discovery and documentation tools (`get_api_methods`, `get_schema`, etc.) do not consume execution time — only `upsert_and_execute_safe_script` and `upsert_and_execute_unsafe_script` do.
    
-   **Output limit**: scripts that produce more than 32 KB of `console.log` output are truncated. If you need to inspect large datasets, ask the assistant to summarise the result rather than dumping it.
    

## Security and Authentication

The DatoCMS MCP server is built around the principle that AI assistants generate untrusted code, and that code should never be able to escape its boundaries. Security is enforced at multiple layers.

###### Native MCP OAuth

There are no API tokens to copy, store, or rotate. The server authenticates through `oauth.datocms.com` using the standard MCP OAuth flow:

-   Tokens never touch your filesystem — your MCP client manages them
-   During the authorization step you can scope access to specific projects and define the level of access (details in the section below)
    
-   Tokens can be revoked at any time from your DatoCMS account settings; the server detects revoked or expired tokens and prompts a re-authentication
-   Single sign-on (SSO) accounts are not currently supported
    

###### Scope of permissions

The installation screen tells you who is asking for permissions. If the app you're installing isn't the one we've verified, the page says "Authorize an Unverified app" and highlights the destination in red, so you can stop and verify which app you're installing.

(Image content)

You also decide how much the app can do. Besides picking which projects the app can access, you can choose a level of access:

-   **Only read content**: read content and media, never write or edit anything.
-   **Read and edit content (recommended for editors)**: the app can also create, publish, and delete content and media. It can do nothing on schema, settings, users, or API tokens.
    
-   **Anything you can (recommended for devs)**: whatever your account can do in DatoCMS, the MCP can as well.
    

> [!NOTE]
> Whichever option you pick is still restricted by your own user permissions in each project. The MCP cannot perform actions that your role is unable to.

If you choose one access level, you can always revisit the config screen to edit this.

###### Isolated script execution

Every script the AI writes runs in an isolated runner, separate from any other user's session:

-   **Network egress is restricted** to DatoCMS APIs, your project's asset storage domain, and a whitelist of well-known image services (Unsplash, Pexels, Pixabay, Picsum)
-   **Credential brokering**: scripts never see your real API token. The runner transparently injects credentials at the network layer, so a script cannot exfiltrate them — even if it tried
    
-   **Read-only enforcement**: when the AI uses the safe script variant, the runner blocks any non-`GET` request at the network layer, and the API itself rejects destructive operations
-   **Type-checking before execution**: scripts are validated by the TypeScript compiler before they're allowed to run, catching errors before they reach the API
    
-   **Per-script timeouts and monthly time budgets** prevent runaway loops and abuse
    

###### Script validation

Beyond runtime isolation, scripts are statically analysed before execution and rejected if they:

-   Use `any` or `unknown` type annotations (which would bypass type-checking)
-   Cast through `never` (a common TypeScript escape hatch)
    
-   Include `@ts-ignore`, `@ts-expect-error`, or `@ts-nocheck` comments
-   Call API methods that the AI has not pre-declared via `get_api_methods` (the verification token system)
    

## Troubleshooting

###### Server not connecting

1.  Verify your client supports remote MCP servers (Claude Code, Claude Desktop / Claude.ai via Connectors, ChatGPT via Developer Mode, Cursor, VS Code, Windsurf, and Antigravity all do)
    
2.  Ensure your MCP client configuration uses the correct URL: `https://mcp.datocms.com`
    
3.  Restart your IDE or client after configuration changes
    
4.  Check that your firewall or corporate proxy allows outbound HTTPS to `mcp.datocms.com` and `oauth.datocms.com`
    

###### Authentication issues

1.  If your token has been revoked or expired, the server will prompt your client to re-authenticate; trigger any DatoCMS tool to start the flow
    
2.  Use the `whoami` tool to verify which DatoCMS account is currently signed in
    
3.  During OAuth authorization, double-check that you granted access to the projects you want the assistant to work with
    
4.  To switch accounts, sign out from your MCP client (the exact step depends on the client) and trigger a fresh authentication
    

###### Script execution failures

1.  If the script hits a timeout, ask the assistant to break it into smaller steps
    
2.  If you've exceeded your monthly execution time budget, wait for the budget to reset or upgrade your plan
    

###### Performance issues

If operations are taking too long:

1.  Break very complex tasks into smaller steps
    
2.  Use more specific prompts to reduce exploration time
    
3.  Check whether a simpler API method can achieve the same result
    

## Feedback & Contributing

We value your feedback to improve the DatoCMS MCP server:

-   **Share your experience**: Let us know what works well and what doesn't
-   **Report bugs and suggestions**: Reach out at [support@datocms.com](mailto:support@datocms.com) — your feedback helps us identify common issues, improve documentation, and enhance the server's capabilities
    

If the AI runs into a bug or gap in our API documentation while using the server, it can use the built-in `report_api_issue` tool to send a structured report directly to our team.

---

# LLM-ready docs — LLM-ready Docs

Source [docs]: https://www.datocms.com/docs/llm-ready-docs.md

Working with AI tools requires high-quality context. DatoCMS addresses this by providing documentation in formats optimized for AI consumption, allowing assistants like Claude, ChatGPT, Cursor, and NotebookLM to deliver **accurate, context-aware responses** about DatoCMS features and APIs.

DatoCMS offers three complementary approaches to accessing documentation for AI tools:

## Complete documentation (llms-full.txt)

The complete DatoCMS documentation compiled into a single, perfectly formatted Markdown file. This includes all 500+ pages of content: API references, guides, migrations, plugins, Content Management API (CMA), Content Delivery API (CDA), and more.

**Access:**

-   **URL**: [`https://www.datocms.com/docs/llms-full.txt`](https://www.datocms.com/docs/llms-full.txt)
    

**What makes it effective:**

-   Clean Markdown with properly formatted code blocks
-   Logical structure maintained across all pages
    
-   Complete context spanning the entire documentation
-   Automatically regenerated with every docs update
    
-   No navigation menus, JavaScript, or extraneous content
    

**Use cases:**

-   Building complex features that require understanding multiple DatoCMS concepts
-   Content migration projects
    
-   Team onboarding and training
-   Creating custom AI assistants with comprehensive DatoCMS knowledge
    

## Documentation index (llms.txt)

A structured index of DatoCMS documentation following the [llms.txt standard](https://llmstxt.org/). Provides an overview of available documentation without the full content.

**Access:**

-   **URL**: [`https://www.datocms.com/docs/llms.txt`](https://www.datocms.com/docs/llms.txt)
    

**Use cases:**

-   Quick reference for documentation structure
-   Discovering available topics and guides
    
-   Navigation for AI tools that support llms.txt format
    

## Single-page Markdown export

Every documentation and blog page includes a "Copy page" dropdown that provides content in AI-friendly formats. This enables quick extraction of specific pages without dealing with HTML formatting issues or web scraping complications.

**Access methods:**

-   **Copy as Markdown**: Click the "Copy page" dropdown on any docs or blog page
-   **Direct URL conversion**: Append `.md` to any page URL to retrieve its Markdown version
    

**Example:**

```plaintext
https://www.datocms.com/docs/content-management-api.md
```

**Use cases:**

-   Troubleshooting specific integrations
-   Exploring particular API methods
    
-   Learning about individual DatoCMS features
-   Providing focused context to AI assistants for targeted questions
    

## Integrations

###### Claude Projects

Claude Projects allow you to attach custom knowledge to Claude conversations. This is particularly effective with `llms-full.txt` for comprehensive DatoCMS expertise.

**Setup:**

1.  Navigate to [claude.ai](https://claude.ai/) and create a new Project
    
2.  Download `llms-full.txt` from [https://www.datocms.com/docs/llms-full.txt](https://www.datocms.com/docs/llms-full.txt)
    
3.  Upload the file to your Project
    
4.  Add custom instructions (see [reference instructions](https://gist.github.com/stefanoverna/e6d225bc3eef2d11bdaae16fb433a5bd#file-datocms-expert-instructions-md))
    
5.  Name your Project (e.g., "DatoCMS Docs")
    

**Capabilities:**

-   Ask migration questions and receive step-by-step instructions
-   Request working TypeScript scripts for content operations
    
-   Plan complex content model migrations with full DatoCMS context
-   Get accurate answers across all conversations in the Project
    

###### Custom GPTs

Build a ChatGPT assistant specialized in DatoCMS using the complete documentation as its knowledge base.

**Setup:**

1.  Go to [ChatGPT GPT Builder](https://chat.openai.com/gpts/editor)
    
2.  Click "Create a GPT"
    
3.  Download `llms-full.txt` from [https://www.datocms.com/docs/llms-full.txt](https://www.datocms.com/docs/llms-full.txt)
    
4.  In the Knowledge section, upload the downloaded file
    
5.  Add instructions such as: "You are a DatoCMS expert. Answer questions using only the provided documentation. Include code examples when relevant." (see [reference instructions](https://gist.github.com/stefanoverna/e6d225bc3eef2d11bdaae16fb433a5bd#file-datocms-expert-instructions-md))
    

**Reference implementation:**

Check the [official DatoCMS Expert GPT](https://chatgpt.com/g/g-68f2397c654081918601c5fa11a21616-datocms-expert) to see a working example.

###### NotebookLM

Google's NotebookLM excels at deep research and learning across large documentation sets.

**Setup:**

1.  Create a new notebook in [NotebookLM](https://notebooklm.google.com/)
    
2.  Add a source → paste `https://www.datocms.com/docs/llms-full.txt`
    
3.  Allow processing to complete (~30 seconds)
    

**Capabilities:**

-   Compare different DatoCMS features and APIs
-   Generate study guides for learning specific topics
    
-   Search across the entire documentation for related concepts
-   Understand relationships between different parts of the system
    

**Use cases:**

-   Onboarding new team members
-   Exploring unfamiliar features
    
-   Research before implementing complex features
    

###### Cursor

Cursor is an AI-powered code editor that benefits from documentation context while coding.

**Setup:**

1.  Open Cursor and type `@Docs`
    
2.  Select "Add new doc"
    
3.  Paste: `https://www.datocms.com/docs/llms-full.txt`
    

**Important:** Always type the `@` symbol manually in the chat interface. Copy-pasting breaks the context reference.

**Capabilities:**

-   Generate Next.js pages that fetch DatoCMS content
-   Add pagination, filtering, or sorting to GraphQL queries
    
-   Debug queries with full knowledge of available fields and filters
-   Write TypeScript scripts that interact with the Content Management API
    

###### Windsurf

Windsurf is another AI coding assistant that supports custom documentation sources.

**Setup:**

1.  Open settings → Documentation
    
2.  Add new documentation source
    
3.  Enter the URL: `https://www.datocms.com/docs/llms-full.txt`
    

**Important:** Always type the `@` symbol manually in the chat interface. Copy-pasting breaks the context reference.

**Capabilities:**

-   Same as Cursor: context-aware code generation for DatoCMS integrations
-   API-aware debugging and query construction
    

###### Other AI tools

Most AI assistants that accept file uploads or URL references can use `llms-full.txt`:

-   **File upload tools**: Download the file and upload directly
-   **URL-based tools**: Reference `https://www.datocms.com/docs/llms-full.txt`
    
-   **Chat interfaces**: Copy and paste relevant sections as needed
    

## Best practices

###### Choosing the right format

**Use single-page Markdown export when:**

-   You need information about a specific feature or API method
-   Working on a focused task that doesn't require broader context
    
-   You want to minimize token usage in your AI assistant
-   Troubleshooting a specific error or implementation detail
    

**Use llms-full.txt when:**

-   Building complex features that span multiple DatoCMS concepts
-   Planning migrations or major content model changes
    
-   Creating a persistent AI assistant with comprehensive DatoCMS knowledge
-   Team members need to learn DatoCMS from scratch
    
-   Working on projects where understanding the full system architecture matters
    

###### Effective prompting

Provide clear, specific prompts that take advantage of the documentation context:

**Good examples:**

-   "Using the Content Management API, write a script to bulk-update all blog posts to add a new field"
-   "How do I migrate content from WordPress to DatoCMS while preserving relationships between posts and categories?"
    
-   "Create a Next.js component that fetches localized content and handles fallbacks according to DatoCMS best practices"
    

**Less effective examples:**

-   "How do I update posts?" (too vague)
-   "Write me a migration script" (missing context about source and requirements)
    
-   "Make a component" (no details about what it should do)
    

###### Iterative refinement

AI assistants work best with iterative feedback:

1.  Start with a clear initial request
    
2.  Review the generated code or answer
    
3.  Provide specific feedback on what needs adjustment
    
4.  Request modifications: "Add error handling" or "Include image optimization"
    

Most tasks succeed on the first attempt, with occasional minor refinements needed through follow-up prompts.

## Token consumption considerations

The impact of using complete documentation (`llms-full.txt`) varies significantly depending on how your AI tool handles knowledge bases.

**Tools with intelligent retrieval (Claude Projects, Custom GPTs, NotebookLM):**

These systems don't load all 500+ pages into every conversation. Instead, they index the documentation and query only relevant sections as needed, retrieving context on-demand based on your questions. Token consumption is not a concern - the system automatically manages what context to include.

**Tools without intelligent retrieval (direct chat interfaces, some coding assistants):**

If you paste the full documentation directly into a chat or use tools that load entire files into context, you may encounter higher token usage per request and potential context window limits. However, the comprehensive context dramatically reduces incorrect or incomplete responses.

> [!PROTIP] Pro tip: Our recommendation
> Use Claude Projects or Custom GPTs for the best balance of comprehensive knowledge and efficient token usage. If your AI tool doesn't offer built-in intelligent retrieval but you need the full documentation, consider implementing RAG (Retrieval-Augmented Generation).
> 
> For tools that don't intelligently manage large knowledge bases, use single-page exports (`.md` URLs) instead. For most practical tasks with the right tools, comprehensive documentation provides better results with acceptable cost.

## Limitations

-   **Markdown conversion quality**: Quality may vary across different pages when using `.md` URL conversion
-   **Documentation updates**: While `llms-full.txt` regenerates automatically, there may be a brief delay after documentation changes
    
-   **AI model capabilities**: Results depend on the underlying AI model's capabilities and training
-   **Context windows**: Some AI tools have limits on how much documentation they can process at once

---

# Translating content with AI — Translating content with AI

Source [docs]: https://www.datocms.com/docs/translating-content-with-ai.md

Managing multilingual content traditionally requires coordinating with translation services or manual work for every content update—a time-consuming process that creates bottlenecks in content publication.

AI-powered translation changes this by providing instant, high-quality translations directly within your editing interface. Modern AI models understand context, preserve formatting, and produce natural-sounding translations that adapt to your content rather than mechanical word-for-word replacements.

The [(Image content)AI Translations](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md) plugin integrates leading AI providers—OpenAI (ChatGPT), Google (Gemini), Anthropic (Claude), and DeepL — directly into DatoCMS. Translate individual fields, entire records, multiple records from table views, or batch-translate whole models. The plugin intelligently handles complex field types including Structured Text, supports contextual translations for better accuracy, and preserves ICU Message Format patterns.

## What you can do

The AI Translations plugin enables editors to:

-   **Translate individual fields**: Use field-level actions to translate content from one locale to another or to all locales at once
-   **Translate entire records**: Use the sidebar panel to translate all localizable fields in a record with a single action
    
-   **Bulk translate from table views**: Select multiple records in any table view and translate them all simultaneously
-   **Translate whole models**: Use the dedicated bulk translations page to translate all records across one or more models
    
-   **Work with multiple AI providers**: Choose between OpenAI (ChatGPT), Google (Gemini), Anthropic (Claude), or DeepL based on your needs and preferences
-   **Preserve complex formatting**: Automatically handle Structured Text, ICU Message Format patterns, HTML, and other complex field types
    
-   **Use contextual translations**: Leverage record context for more accurate, consistent translations that understand specialized terminology
-   **Enforce terminology with glossaries**: Use DeepL glossaries to ensure preferred translations for specific terms across all content
    

The plugin works with all DatoCMS field types including single-line strings, markdown, structured text, modular content, SEO fields, and media fields with metadata.

## Requirements

-   DatoCMS project with at least two locales configured
-   API key from at least one supported provider:
    
    -   **OpenAI**: Regular secret key from [platform.openai.com](https://platform.openai.com/)
        
    -   **Google (Gemini)**: API key from GCP project with Generative Language API enabled
        
    -   **Anthropic (Claude)**: API key from [console.anthropic.com](https://console.anthropic.com/)
        
    -   **DeepL**: API key (Free or Pro)
        

## Installation

Install the AI Translations plugin from the DatoCMS Marketplace:

1.  Navigate to your DatoCMS project
    
2.  Go to **Settings** → **Plugins**
    
3.  Click **Add** → **From Marketplace**
    
4.  Search for "AI Translations"
    
5.  Click **Install** on the [AI Translations plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md)
    

The plugin will appear in your project's plugin list and be ready for configuration.

## Configuration

Access the plugin configuration screen from **Settings** → **Plugins** → **AI Translations** → **Settings**.

###### Choose your AI provider

**Vendor selection**: Select your preferred translation provider from the dropdown:

-   **DeepL** (recommended): Fastest response times, professional translation quality, specialized for language translation with glossary support
-   **OpenAI** (ChatGPT): Fast, widely available, excellent for general content
    
-   **Google** (Gemini): Cost-effective with strong multilingual capabilities
-   **Anthropic** (Claude): High-quality translations with nuanced understanding
    

###### Configure provider credentials

**For OpenAI:**

-   **OpenAI API Key**: Paste your API key from [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
-   **GPT Model**: Select your preferred model
    

**For Google (Gemini):**

-   **Google API Key**: Paste your API key from Google Cloud Console
-   **Gemini Model**: Select your preferred model
    

**For Anthropic (Claude):**

-   **Anthropic API Key**: Paste your API key from [console.anthropic.com](https://console.anthropic.com/)
-   **Anthropic Model**: Select from available Claude models for optimal quality
    

**For DeepL:**

-   **DeepL API Key**: Paste your API key from [deepl.com/pro-api](https://www.deepl.com/pro-api)
-   **Use DeepL Free endpoint**: Enable if your key ends with `:fx`
    
-   **Formality**: Choose translation formality level (default, more, less)
-   **Advanced settings**: Configure glossaries and formatting options
    

###### Translatable field types

Select which field editor types should show translation actions:

-   Single line string
-   Markdown
    
-   HTML Editor (WYSIWYG)
-   Textarea
    
-   Slug
-   JSON
    
-   SEO fields
-   Structured Text
    
-   Modular Content
-   Media Fields (translates metadata like title, alt text)
    

Enable only the field types you need to translate. This keeps the interface clean and prevents accidental translations of fields that shouldn't change across locales.

###### Translation features

-   **Translate Whole Record**: Enable the sidebar panel that translates all localizable fields in a record with one action
-   **Translate Bulk Records**: Enable bulk translation from table views, allowing editors to select and translate multiple records simultaneously
    
-   **AI Bulk Translations Page**: Enable the dedicated page for translating entire models at once (accessible from **Settings** → **AI Bulk Translations**)
    

###### Prompt customization

**Prompt Template**: Customize how the AI is instructed to translate content. Use placeholders:

-   `{fieldValue}`: The content to translate
-   `{fromLocale}`: Source language (e.g., "en-US")
    
-   `{toLocale}`: Target language (e.g., "pt-BR")
-   `{recordContext}`: Automatically generated context about the record
    

**Default prompt:**

```plaintext
Translate the following text from {fromLocale} to {toLocale}.
Preserve all formatting, HTML tags, and placeholder variables.
{recordContext}

Text to translate:
{fieldValue}
```

The `{recordContext}` placeholder provides the AI with information about other fields in the record, improving translation accuracy by understanding specialized terminology and maintaining consistency across related fields.

###### Access restrictions

-   **Models to exclude**: Specify model API keys that should not show translation features
-   **Roles to exclude**: Restrict which user roles can access translation features
    
-   **API Keys to exclude**: Block specific API keys from using the plugin
    

###### Debugging

-   **Enable debugging**: Turn on detailed console logging to troubleshoot translation issues
    

### Security best practices

**API key storage:**

-   Keys are stored in plugin settings and used client-side
-   Never share your DatoCMS workspace publicly with API keys configured
    
-   Rotate keys periodically
    

**Key restrictions:**

-   **OpenAI**: Use regular secret keys, not publishable keys; set usage limits in your OpenAI dashboard
-   **Google**: Restrict keys by HTTP referrer (`https://admin.datocms.com/*`) and enable only the Generative Language API
    
-   **Anthropic**: Set spending limits in the Anthropic console
-   **DeepL**: Set usage limits in your DeepL account dashboard
    

The plugin automatically redacts API keys from debug logs to prevent accidental exposure.

# Using the plugin

#### Field-level translations

Translate individual fields directly in the record editor:

1.  Open any record with localizable fields
    
2.  Click the field's dropdown menu (three dots in the top-right corner of the field)
    
3.  Select **Translate to** → Choose a target locale or **All locales**
    
4.  The plugin generates the translation and updates the field automatically
    

**Translate from a different source:**

-   Select **Translate from** → Choose a source locale
-   The current locale's field will be filled with translated content from the selected source locale
    

This is useful when your primary content is in a locale other than the default, or when you want to translate from a recently updated locale.

#### Whole-record translations

Translate all localizable fields in a record at once:

1.  Open a record that has multiple locales
    
2.  The **DatoGPT Translate** panel appears in the sidebar (if enabled in settings)
    
3.  Select your source locale (the locale containing the content to translate)
    
4.  Select your target locale (the locale to translate into)
    
5.  Click **Translate Entire Record**
    
6.  All translatable fields update with AI-generated translations
    

This workflow is efficient for content editors who need to create complete translations for newly published content or update existing translations when source content changes.

#### Bulk translations from table view

Translate multiple records simultaneously from any model's table view:

1.  Navigate to any model in the **Content** area
    
2.  Switch to table view if not already displayed
    
3.  Select multiple records by checking the boxes on the left
    
4.  Click the **three dots** dropdown in the bottom bar
    
5.  Choose **Translate records**
    
6.  Select source and target locales
    
7.  Click **Start Translation**
    
8.  A progress modal shows translation status for all selected records
    

This is ideal for translating batches of content after bulk imports, when launching a new locale, or when updating multiple related records.

#### Bulk translations page

Translate entire models using the dedicated bulk translations page:

1.  Go to **Settings** → **AI Bulk Translations**
    
2.  Select your source locale (containing the content to translate)
    
3.  Select your target locale (to receive translations)
    
4.  Choose one or more models to translate
    
    -   Block models are automatically excluded
        
    -   Only models with localizable fields appear
        
5.  Click **Start Bulk Translation**
    
6.  The progress modal displays real-time status as records are processed
    

This workflow is designed for large-scale translation operations: launching new locales, migrating content, or keeping translations synchronized across your entire content base.

#### Contextual translations

The plugin supports context-aware translations through the `{recordContext}` placeholder in your prompt template.

**How it works:**

When translating a field, the plugin automatically generates a summary of other fields in the same record and includes it in the translation prompt. This gives the AI understanding of:

-   Specialized terminology used in the record
-   The overall topic and subject matter
    
-   Related content in other fields
-   Appropriate tone and style
    

**Benefits:**

-   More accurate translations of technical terms and industry jargon
-   Consistent terminology across all fields in a record
    
-   Better understanding of context improves translation quality
-   Appropriate formality and tone based on content type
    

> [!POSITIVE] An example
> For a product record with fields like "Product Name: CloudSync Pro" and "Category: Enterprise Software", the AI understands it's translating technical content and maintains appropriate terminology rather than translating brand names or technical terms.

### ICU Message Format support

The plugin intelligently handles [**ICU Message Format**](https://unicode-org.github.io/icu/userguide/format_parse/messages/) strings, preserving complex pluralization and selection logic during translation.

**Smart masking:**

-   Simple variables like `{name}` are masked (protected from translation)
-   ICU structures like `{count, plural, one {...} other {...}}` are passed to the AI with explicit instructions to preserve the format
    

**What gets translated:**

The AI translates only the human-readable content inside ICU structures while preserving:

-   Variable names and placeholders
-   Keywords (`plural`, `select`, `one`, `other`, etc.)
    
-   Structural syntax
-   Number signs (`#`) and formatting codes
    

**Example:**

English:

```plaintext
You have {count, plural, one {# message} other {# messages}}
```

Portuguese translation:

```plaintext
Você tem {count, plural, one {# mensagem} other {# mensagens}}
```

The structure remains identical; only "message" and "messages" are translated to "mensagem" and "mensagens".

### DeepL glossaries

When using DeepL as your translation provider, you can enforce preferred terminology through glossaries.

**Requirements:**

-   DeepL API key with glossary access (check your plan)
-   Glossary IDs from your DeepL account
    

**Configuration:**

1.  Navigate to plugin settings → DeepL section
    
2.  Expand **Advanced settings**
    
3.  Configure glossaries: **Default glossary ID**: Used for all translations unless overridden **Glossaries by language pair**: Map specific language pairs to specific glossaries
    

**Configuration examples:**

**Single language pair:**

If you only translate from English to German:

-   **Default glossary ID**: `gls-12345` (your EN→DE glossary)
-   **Glossaries by language pair**: *(leave empty)*
    

**Multiple language pairs:**

If you translate to multiple languages:

-   **Default glossary ID**: *(leave empty)*
-   **Glossaries by language pair**:`EN->DE=gls-german123   EN->FR=gls-french456   EN->PT-BR=gls-portuguese789`
    

**Fallback strategy:**

Use specific glossaries for main languages and a default for others:

-   **Default glossary ID**: `gls-fallback999`
-   **Glossaries by language pair**:`EN->DE=gls-german123`, `EN->FR=gls-french456`
    

**Mapping syntax:**

One entry per line. Supported formats:

```plaintext
EN->DE=gls-abc123
en-US->pt-BR=gls-xyz789
fr→it gls-123                 # alt arrow and delimiter
*->pt-BR=gls-777              # wildcard: any source to target
EN->*=gls-555                 # wildcard: source to any target
pt-BR=gls-777                 # shorthand for *->pt-BR
```

**Creating glossaries:**

Create and manage glossaries using the DeepL API (not through the plugin):

**List existing glossaries:**

Terminal window

```bash
curl -H "Authorization: DeepL-Auth-Key $DEEPL_AUTH_KEY" \
     https://api.deepl.com/v2/glossaries
```

**Create a new glossary:**

Terminal window

```bash
curl -X POST https://api.deepl.com/v2/glossaries \
  -H "Authorization: DeepL-Auth-Key $DEEPL_AUTH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marketing-EN-DE",
    "source_lang": "EN",
    "target_lang": "DE",
    "entries_format": "tsv",
    "entries": "CTA\tCall-to-Action\nlead magnet\tLeadmagnet"
  }'
```

Copy the returned `glossary_id` and paste it into the plugin settings.

**Testing:**

1.  Create a small glossary with an obvious term
    
2.  Add the glossary ID to plugin settings
    
3.  Translate a field containing that term
    
4.  Verify the glossary translation appears in the result
    

### Workflow recommendations

**For individual content updates:**

-   Use field-level translations for quick updates to specific fields
-   Use whole-record translation when creating new localized versions
    

**For launching new locales:**

1.  Configure the new locale in DatoCMS
    
2.  Use the bulk translations page to translate all models at once
    
3.  Review and refine translations in critical content
    

**For ongoing content maintenance:**

-   Translate new records immediately after creation using whole-record translation
-   Use bulk table view translations when updating multiple related records
    
-   Enable debugging temporarily if you encounter translation issues
    

### Quality optimization

**Improve translation accuracy:**

-   Enable `{recordContext}` in your prompt template for context-aware translations
-   Use glossaries (DeepL) to enforce preferred terminology
    
-   Customize the prompt template to include industry-specific instructions
-   Review AI-generated translations before publishing, especially for marketing copy
    

**Handle special content types:**

-   **Technical content**: Use glossaries or custom prompts mentioning technical terminology
-   **Marketing copy**: Consider using higher-quality models (e.g., `gpt-4.1`, `gemini-2.5-pro`)
    
-   **Legal content**: Always have professional review; AI is a starting point, not final output
    

## Troubleshooting

###### API key issues

**Invalid API Key error:**

-   Verify the key matches the selected vendor
-   Check that the key hasn't expired or been revoked
    
-   Ensure there are no extra spaces or quotes around the key
-   Test the key directly with the provider's API playground
    

**Rate limit / quota errors:**

-   Reduce translation concurrency by translating smaller batches
-   Switch to a lighter model (e.g., `gpt-4o-mini`, `gemini-2.5-flash-lite`)
    
-   Check your provider's dashboard for usage limits
-   Upgrade your plan if you consistently hit limits
    

###### Translation issues

**Translations not appearing:**

-   Verify at least two locales are configured in your DatoCMS project
-   Check that the field type is enabled in **Translatable Field Types** settings
    
-   Ensure the field is set as localizable in the model schema
-   Check browser console for errors (enable debugging in plugin settings)
    

**Poor translation quality:**

-   Add `{recordContext}` to your prompt template for context-aware translations
-   Try a more advanced model
    
-   Customize the prompt template with specific instructions
-   Use DeepL with glossaries for consistent terminology
    

**Model not found:**

-   Verify the exact model ID exists for your account/region
-   Check spelling and capitalization
    
-   Refresh available models by re-entering your API key
    

###### DeepL-specific issues

**Wrong endpoint error:**

-   Free keys (ending in `:fx`) require "Use DeepL Free endpoint" enabled in plugin settings
-   Pro keys should have this setting disabled
    

**Glossary not working:**

-   Verify the glossary ID exists in your DeepL account
-   Check that glossary languages match your translation direction
    
-   Ensure the glossary was created for the correct language pair
-   Test with a known term from your glossary
    

###### Performance issues

**Slow translations:**

-   Switch to faster models (`gpt-4.1-mini`, `gemini-2.5-flash`)
-   Translate smaller batches instead of entire models at once
    
-   Check your internet connection stability
-   Verify you're not hitting rate limits (check provider dashboard)
    

**Bulk operations timing out:**

-   Reduce the number of records per batch
-   Translate one model at a time instead of multiple models
    
-   Use a faster model for initial translations, then refine critical content manually
    

## Limitations

-   **Browser-based execution**: API keys are used client-side; keep workspaces private
-   **Locale configuration**: Projects must have at least two locales for translations to function
    
-   **Field type support**: Only configured field types show translation actions
-   **Provider availability**: Translation quality and speed depend on selected provider and model
    
-   **Cost**: Translation usage incurs costs from your chosen AI provider
-   **No translation memory**: Each translation is independent; the plugin doesn't maintain a translation memory
    
-   **Manual review recommended**: AI translations should be reviewed before publishing, especially for critical content

---

# Visual Editing — Visual Editing

Source [docs]: https://www.datocms.com/docs/visual-editing.md

Visual Editing lets content editors **click directly on any element of your website** to edit it in DatoCMS, without hunting through forms and fields. Combined with draft content and real-time updates, editors see **saved changes reflected** on the page they're editing.

> [!POSITIVE] Available on every current plan
> Visual Editing is included in every current DatoCMS plan (including the free Developer plan, Professional, and Enterprise) at no additional charge. It works with all your primary and sandbox environments.
> 
> Note: Visual Editing may **not** be available for some older, grandfathered "Legacy" plans. If you're not sure, please [contact support](https://www.datocms.com/support.md) and we can check for you.

## The problem it solves

In a traditional headless CMS workflow, there's a disconnect between what editors see in the CMS (forms, fields, JSON) and what visitors see on the website. Editors have to make changes in the CMS, save, wait for a preview to rebuild, switch tabs to check the result, and go back to make further adjustments. This loop is slow, error-prone, and frustrating — especially for non-technical editors who think in terms of "the headline on the homepage", not "the `title` field on the `home_page` record".

Visual Editing closes this gap entirely. Editors work with **the actual website** (the real layout, the real typography, the real context) and every piece of content becomes a **direct entry point back into the CMS**.

## Two ways to use it

Visual Editing supports two workflows that serve different editing styles. Both can coexist, and editors pick whichever suits the task at hand.

###### Browsing the website directly

Editors visit the website in draft mode (typically via a preview URL) and interact with content right there. When they hover over any editable element (a title, body text, an image alt text) a subtle overlay appears. Clicking it **opens DatoCMS in a new browser tab**, navigated directly to the exact field that controls that piece of content. There's no guesswork about "where does this text live in the CMS?".

(Video content)

Click-to-edit overlays

###### Side-by-side editing inside DatoCMS

The [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) is the bridge that connects your website to your DatoCMS project. Once installed, it brings a **live preview of your website directly into the DatoCMS interface**, turning the CMS from a form-based tool into a visual editing environment.

The plugin adds several touchpoints to the DatoCMS UI:

-   **A "Visual" tab** in the main navigation: a full-screen, side-by-side editing view where the website preview sits next to the editing panel. Editors click on any element in the preview, and the corresponding record and field open right there. The tab includes:
    
    -   An address bar for navigating the preview
        
    -   Viewport controls for testing responsive layouts
        
    -   A frontend selector for switching between environments (e.g. production vs. staging)
        
-   **Preview links in the record sidebar**: when editors open any record, they see quick links to view that content on the actual website. The plugin determines which URL corresponds to each record by calling an API endpoint on your frontend.
-   **A full iframe preview in the sidebar**: beyond just links, editors can expand an inline preview of the page directly in the sidebar, with viewport presets (mobile, tablet, desktop) and auto-reload on save.
    

The plugin supports **multiple frontends**, so if your content powers different websites or environments, editors can switch between them from a single dropdown.

###### Bidirectional navigation

The connection between the CMS and the preview **works in both directions**. When editors browse through records in DatoCMS, the preview navigates to the corresponding page automatically. And when they click around in the website preview, DatoCMS follows along, opening the relevant record. This means editors can start from whichever side feels natural (the content or the page) and **the other side stays in sync**.

(Video content)

Side-by-side editing

## Real-time feedback

In both workflows, when combined with [Real-time Updates](/docs/real-time-updates-api.md), editors see their saved changes reflected on the preview they're editing. There's no need to reload: the preview updates live, giving editors immediate confidence that their changes look right in context.

## The building blocks

Visual Editing is not a single feature, but the combination of several DatoCMS capabilities that can be adopted incrementally:

-   [**Draft Mode**](/docs/general-concepts/draft-published.md): lets your frontend serve unpublished content during preview sessions
-   [**Real-time Updates**](/docs/real-time-updates-api.md): pushes content changes to the preview without a page refresh
    
-   **Content Link**: adds click-to-edit overlays connecting frontend elements to CMS fields
-   [**Web Previews plugin**](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md): embeds the preview inside DatoCMS for a unified editing experience
    

You can **adopt each capability independently** and stop at any point — each one delivers value on its own. But the full combination is where the experience really comes together.

The integration of all these layers is designed to be straightforward, especially if you start from one of our [tech starter kits](https://www.datocms.com/marketplace/starters.md). These official scaffolds come pre-configured with Draft Mode, Real-time Updates, Content Link, and Web Previews already wired together, so you get a fully working Visual Editing setup with minimal effort and can focus on your content and design.

## How it works technically

###### 1\. Embedded metadata

When your frontend fetches draft content from the [Content Delivery API](/docs/content-delivery-api.md), DatoCMS can use a technique called [steganography](https://en.wikipedia.org/wiki/Steganography) to **embed invisible metadata into text fields**. This metadata, hidden using special Unicode characters that don't affect how text appears visually, carries information about **which record and field produced each piece of text**. Our plugins then parse this invisible metadata to match the rendered DOM nodes to the DatoCMS fields that they came from.

You enable this by passing these options when querying the CDA using our [cda-client](https://github.com/datocms/cda-client):

```javascript
executeQuery(query, {
  includeDrafts: true,
  contentLink: 'v1', // Must be exactly 'v1'
  baseEditingUrl: 'https://your-project.admin.datocms.com', // Your project domain
});
```

> [!WARNING] Invisible metadata can break string comparisons, JS, CSS, etc.
> Because this invisible metadata is injected into the string values themselves, it can lead to unexpected situations where strings that look visually the same are actually different:
> 
> ```javascript
> const firstString = 'a'; // Just the letter
> const secondString = 'a󠁡'; // This one has invisible metadata at the end
> 
> 
> /* They are not the same values! */
> firstString == secondString; // FALSE
> 
> 
> /* They are not the same lengths */
> firstString.length; // 1
> secondString.length; // 3 (!) because of the invisible chars
> ```
> 
> This can break things like CSS layouts where you use field values like `left` or `right` or `blue` or `#FF0034`, Javascript equality comparisons like `==` or `===` , string length checks, regex end-of-line checks, etc.
> 
> Fortunately, the fix is straightforward: You can use the `stripStega()` utility from [@datocms/content-link](https://github.com/datocms/content-link#low-level-utilities) to sanitize the string before use:
> 
> ```javascript
> import {stripStega} from '@datocms/content-link';
> const firstString = 'a'; // Just the letter
> const secondString = 'a󠁡'; // This one has invisible metadata at the end
> 
> 
> /* Strip stega before any string comparisons */
> stripStega(firstString) == stripStega(secondString); // TRUE now!
> ```
> 
> **TL;DR: Use** [**stripStega()**](https://github.com/datocms/content-link#low-level-utilities) **any time a string is used as a raw literal value in comparisons or logic or layout.**

###### 2\. Content Link component

A `<ContentLink />` component (available for every supported framework) **scans the page for this embedded metadata** and renders interactive overlays on hover. Overlays have a configurable `hue` property that accepts values from 0-359, allowing you to choose the color that best matches your frontend/branding. Clicking an overlay opens DatoCMS at the exact field — either in a new tab or inside the Web Previews plugin panel if the site is loaded within DatoCMS. The component **detects its context automatically**, so no code changes are needed on your frontend to support both modes.

You render this component in your root layout, only when draft mode is active:

```plaintext
{draftModeEnabled && <ContentLink />}
```

###### 3\. Web Previews plugin

The [Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) plugin communicates with your frontend through two integration points:

-   **Preview Links API**: an endpoint on your frontend that receives record information from DatoCMS (the record ID, model API key, and current locale) and returns the URL where that record can be previewed. This is how the plugin knows which page to show for each record.
-   **Draft Mode route**: your existing route that activates draft mode and redirects to the preview page. The plugin calls this to ensure the iframe always loads draft content.
    

The Content Link component on your frontend and the plugin communicate automatically via an iframe messaging protocol: when an editor clicks on content in the preview, the frontend tells the plugin which record to open, and the plugin navigates the CMS accordingly.

###### Setting up the plugin

1.  Install the [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) from the DatoCMS marketplace
    
2.  Add a frontend with:
    
    -   **Name**: a label for this frontend (e.g. "Production", "Staging")
        
    -   **Preview Links API endpoint**: `https://yoursite.com/api/preview-links?token=your-secret`
        
    -   **Draft Mode route**: `https://yoursite.com/api/draft-mode/enable?token=your-secret`
        
3.  Optionally configure custom viewport presets, multiple frontends, and sidebar display preferences
    

## Framework integration guides

Each framework has its own guide covering the full setup (Draft Mode, Real-time Updates, Content Link, and Web Previews) with working code from our official starter kits:

-   [Next.js Visual Editing](/docs/next-js/visual-editing.md)
-   [Nuxt Visual Editing](/docs/nuxt/visual-editing.md)
    
-   [SvelteKit Visual Editing](/docs/svelte/visual-editing.md)
-   [Astro Visual Editing](/docs/astro/visual-editing.md)
    

###### Underlying library

All framework integrations are built on top of [`@datocms/content-link`](https://github.com/datocms/content-link), a framework-agnostic library that handles metadata detection, overlay rendering, and communication with the Web Previews plugin. If you're using a framework we don't have an SDK for, you can integrate directly with this library.

## Comparison with Vercel Content Link

Vercel offers a similar feature also called [Content Link](https://vercel.com/docs/workflow-collaboration/edit-mode#content-link) (part of Vercel's Edit Mode). DatoCMS **fully integrates with Vercel Content Link** — the Content Delivery API can embed the metadata that Vercel expects, so you can use Vercel's overlay UI if you prefer. We have a [dedicated guide for setting up Vercel Content Link with DatoCMS](/docs/content-link/how-to-use-content-link.md).

Both approaches use the same underlying technique (invisible metadata embedded in text fields), but there are key differences:

-   **Plan requirements**: Vercel Content Link requires a Vercel Pro or Enterprise plan. DatoCMS Visual Editing works on every plan, including Free.
-   **Environment**: Vercel Content Link only works on Vercel preview deployments. DatoCMS Visual Editing works in any environment (development, staging, or production) and on any hosting platform.
    
-   **Editing experience**: Vercel Content Link opens the CMS in a new tab. DatoCMS Visual Editing (with the Web Previews plugin) offers a side-by-side editing experience directly within DatoCMS, with bidirectional navigation and real-time updates.
    

> [!WARNING] Don't use both simultaneously
> If you deploy on Vercel, you should choose one approach or the other — using both simultaneously would result in duplicate overlays. DatoCMS Visual Editing provides a more comprehensive and widely available experience, while Vercel Content Link may be a simpler option if you're already on a Vercel Pro or Enterprise plan and don't need the side-by-side editing workflow.

---

# Working with Structured Text — Structured Text and \`dast\` format

Source [docs]: https://www.datocms.com/docs/structured-text/dast.md

Structured Text content is stored as a JSON object consisting of two mandatory keys:

-   `document`: the content, expressed as a [`unist`](https://github.com/syntax-tree/unist) tree;
-   `schema`: a string that specifies the unist dialect used inside the `document` itself.
    

```json
{
  "schema": "dast",
  "document": {
    "type": "root",
    "children": [...]
  }
}
```

Generally speaking, you want to set the `schema` key to the `dast` dialect (which stands for **D**atoCMS **A**bstract **S**yntax **T**ree), so that:

-   you can take advantage of the default **Structured Text** editor that DatoCMS offers;
-   you can reinforce a number of additional validations to ensure consistency within the document.
    

If you would like to to use a custom [unist](https://github.com/syntax-tree/unist) format rather than `dast`, please [let us know!](https://www.datocms.com/support.md?topics=feature-request)

### DatoCMS Abstract Syntax Tree (`dast`) specification

The `dast` specification adheres to the [Unified](https://unifiedjs.com/) collective, which offers a large ecosystem of utilities to parse, transform, manipulate, convert, and serialize content of any kind.

Unified is implemented and used as foundation by several popular libraries, such as [rehype](https://github.com/rehypejs/rehype) (HTML parser), [remark](https://github.com/remarkjs/remark) (Markdown parser) and the [MDX project](https://mdxjs.com/). All these different projects are able to integrate with each other due to the fact that, to describe the content they treat, they all use the same common JSON format called [`unist`](https://github.com/syntax-tree/unist).

(Image content)

Just like HTML, a `dast` document is composed of nodes within nodes:

-   Each node has a type attributed called `type`
-   The top-level node in the `dast` specification must be of type `root`
    
-   Most nodes have a `children` attribute to specify the nodes it contains
-   The leaves of the tree are nodes of type `span`, which do not offer a `children` attribute but store the final text as a string in their `value` attribute
    
-   The specs define exactly which attributes and children each node permits.
    

Let's look at an example:

```json
{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "level": 1,
      "children": [
        {
          "type": "span",
          "marks": [],
          "value": "This is a title!"
        }
      ]
    },
    {
      "type": "paragraph",
      "children": [
        {
          "type": "span",
          "value": "This is a "
        }
        {
          "type": "span",
          "marks": ["strong"],
          "value": "paragraph!"
        }
      ]
    },
    {
      "type": "list",
      "style": "bulleted",
      "children": [
        {
          "type": "listItem",
          {
            "type": "paragraph",
            "children": [
              {
                "type": "span",
                "value": "And this is a list!"
              }
            ]
          },
        }
      ]
    }
  ]
}
```

### Working with `dast` documents

The package [`datocms-structured-text-utils`](https://github.com/datocms/structured-text/tree/main/packages/utils) offers JavaScript nodes definitions, Typescript types and type guards and many tree manipulation utilities.

Additionally, you can take advantage of [several `unist` utilities](https://github.com/syntax-tree/unist#list-of-utilities) to work with nodes in a `dast` document. For example, you can compose and assemble a document with [`unist-builder`](https://github.com/syntax-tree/unist-builder), select nodes with a CSS-like syntax using [`unist-util-select`](https://github.com/syntax-tree/unist-util-select) or have a compact representation of the document via [`unist-util-inspect`](https://github.com/syntax-tree/unist-util-inspect):

```javascript
import u from 'unist-builder';
import inspect from 'unist-util-inspect';

const document =
  u('root', [
    u('heading', { level: 1}, [
      u('span', 'This is the title!')
    ]),
    u('paragraph', [
      u('span', 'And '),
      u('span', { marks: ['strong'] }, 'this'),
      u('span', ' is a paragraph!')
    ])
  ]);

console.log(inspect(document));

root[2]
├─0 heading[1]
│   │ level: 1
│   └─0 span "This is the title!"
└─1 paragraph[3]
    ├─0 span "And "
    ├─1 span "this"
    │     marks: ["strong"]
    └─2 span " is a paragraph!"
```

### Converting HTML to Structured Text and vice versa

These are the utilities contained within **datocms/structured-text**:

**Conversion utilities**

-   [`datocms-html-to-structured-text`](https://github.com/datocms/structured-text/tree/main/packages/html-to-structured-text) — Convert HTML/Markdown into Structured Text
    

**Rendering utilities**

-   [`datocms-structured-text-to-plain-text`](https://github.com/datocms/structured-text/tree/main/packages/to-plain-text) — Render Structured Text as plain text
-   [`datocms-structured-text-to-markdown`](https://github.com/datocms/structured-text/tree/main/packages/to-markdown) — Render Structured Text as Markdown
    
-   [`datocms-structured-text-to-html-string`](https://github.com/datocms/structured-text/tree/main/packages/to-html-string) — Render Structured Text as an HTML string
-   [`datocms-structured-text-to-dom-nodes`](https://github.com/datocms/structured-text/tree/main/packages/to-dom-nodes) — Transform Structured Text into a list of DOM nodes
    

**Framework components**

-   **React** → [`<StructuredText />`](https://github.com/datocms/react-datocms#structured-text)
-   **Vue** → [`<datocms-structured-text />`](https://github.com/datocms/vue-datocms#structured-text)
    
-   **Svelte / SvelteKit** → [`<StructuredText />`](https://github.com/datocms/datocms-svelte/tree/main/src/lib/components/StructuredText)
-   **Astro** → [`<StructuredText />`](https://github.com/datocms/astro-datocms/tree/main/src/StructuredText)
    

### JSON Schema for `dast`

The latest `dast` format specification is always available at the following URL:

[https://site-api.datocms.com/docs/dast-schema.json](https://site-api.datocms.com/docs/dast-schema.json)

### `root`

Every `dast` document MUST start with a `root` node.

It allows the following children nodes: [`paragraph`](/docs/structured-text/dast.md#paragraph), [`heading`](/docs/structured-text/dast.md#heading), [`list`](/docs/structured-text/dast.md#list), [`code`](/docs/structured-text/dast.md#code), [`blockquote`](/docs/structured-text/dast.md#blockquote), [`block`](/docs/structured-text/dast.md#block) and [`thematicBreak`](/docs/structured-text/dast.md#thematicBreak).

```json
{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "level": 1,
      "children": [
        {
          "type": "span",
          "value": "Title"
        }
      ]
    },
    {
      "type": "paragraph",
      "children": [
        {
          "type": "span",
          "value": "A simple paragraph!"
        }
      ]
    }
  ]
}
```

### `paragraph`

A `paragraph` node represents a unit of textual content.

It allows the following children nodes: [`span`](/docs/structured-text/dast.md#span), [`link`](/docs/structured-text/dast.md#link), [`itemLink`](/docs/structured-text/dast.md#itemLink), [`inlineItem`](/docs/structured-text/dast.md#inlineItem) and [`inlineBlock`](/docs/structured-text/dast.md#inlineBlock).

```json
{
  "type": "paragraph",
  "children": [
    {
      "type": "span",
      "value": "A simple paragraph!"
    }
  ]
}
```

### `span`

A `span` node represents a text node. It might optionally contain decorators called `marks`. It is worth mentioning that you can use the `\n` newline character to express line breaks.

It does not allow children nodes.

```json
{
  "type": "span",
  "marks": ["highlight", "emphasis"],
  "value": "Some random text here, move on!"
}
```

### `link`

A `link` node represents a normal hyperlink. It might optionally contain a number of additional custom information under the `meta` key. You can also link to DatoCMS records using the [`itemLink`](/docs/structured-text/dast.md#itemLink) node.

It allows the following children nodes: [`span`](/docs/structured-text/dast.md#span).

```json
{
  "type": "link",
  "url": "https://www.datocms.com/",
  "meta": [
    { "id": "rel", "value": "nofollow" },
    { "id": "target", "value": "_blank" }
  ],
  "children": [
    {
      "type": "span",
      "value": "The best CMS in town"
    }
  ]
}
```

### `itemLink`

An `itemLink` node is similar to a [`link`](/docs/structured-text/dast.md#link) node node, but instead of linking a portion of text to a URL, it links the document to another record present in the same DatoCMS project.

It might optionally contain a number of additional custom information under the `meta` key.

If you want to link to a DatoCMS record without having to specify some inner content, then please use the [`inlineItem`](/docs/structured-text/dast.md#inlineItem) node.

It allows the following children nodes: [`span`](/docs/structured-text/dast.md#span).

```json
{
  "type": "itemLink",
  "item": "38945648",
  "meta": [
    { "id": "rel", "value": "nofollow" },
    { "id": "target", "value": "_blank" }
  ],
  "children": [
    {
      "type": "span",
      "value": "Matteo Giaccone"
    }
  ]
}
```

### `inlineItem`

An `inlineItem`, similarly to [`itemLink`](/docs/structured-text/dast.md#itemLink), links the document to another record but does not specify any inner content (children).

It can be used in situations where it is up to the frontend to decide how to present the record (ie. a widget, or an `<a>` tag pointing to the URL of the record with a text that is the title of the linked record).

It does not allow children nodes.

```json
{
  "type": "inlineItem",
  "item": "74619345"
}
```

### `inlineBlock`

It does not allow children nodes.

```json
{
  "type": "inlineBlock",
  "item": "1238455312"
}
```

### `heading`

An `heading` node represents a heading of a section. Using the `level` attribute you can control the rank of the heading.

It allows the following children nodes: [`span`](/docs/structured-text/dast.md#span), [`link`](/docs/structured-text/dast.md#link), [`itemLink`](/docs/structured-text/dast.md#itemLink), [`inlineItem`](/docs/structured-text/dast.md#inlineItem) and [`inlineBlock`](/docs/structured-text/dast.md#inlineBlock).

```json
{
  "type": "heading",
  "level": 2,
  "children": [
    {
      "type": "span",
      "value": "An h2 heading!"
    }
  ]
}
```

### `list`

A `list` node represents a list of items. Unordered lists must have its `style` field set to `bulleted`, while ordered lists, instead, have its `style` field set to `numbered`.

It allows the following children nodes: [`listItem`](/docs/structured-text/dast.md#listItem).

```json
{
  "type": "list",
  "style": "bulleted",
  "children": [
    {
      "type": "listItem",
      "children": [
        {
          "type": "paragraph",
          "children": [
            {
              "type": "span",
              "value": "This is a list item!"
            }
          ]
        }
      ]
    }
  ]
}
```

### `listItem`

A `listItem` node represents an item in a list.

It allows the following children nodes: [`paragraph`](/docs/structured-text/dast.md#paragraph) and [`list`](/docs/structured-text/dast.md#list).

```json
{
  "type": "listItem",
  "children": [
    {
      "type": "paragraph",
      "children": [
        {
          "type": "span",
          "value": "This is a list item!"
        }
      ]
    }
  ]
}
```

### `code`

A `code` node represents a block of preformatted text, such as computer code.

It does not allow children nodes.

```json
{
  "type": "code",
  "language": "javascript",
  "highlight": [1],
  "code": "function greetings() {\n  console.log('Hi!');\n}"
}
```

### `blockquote`

A `blockquote` node is a containter that represents text which is an extended quotation.

It allows the following children nodes: [`paragraph`](/docs/structured-text/dast.md#paragraph).

```json
{
  "type": "blockquote",
  "attribution": "Oscar Wilde",
  "children": [
    {
      "type": "paragraph",
      "children": [
        {
          "type": "span",
          "value": "Be yourself; everyone else is taken."
        }
      ]
    }
  ]
}
```

### `block`

Similarly to [Modular Content](/docs/content-modelling/modular-content.md) fields, you can also embed block records into Structured Text. A `block` node stores a reference to a DatoCMS block record embedded inside the `dast` document.

This type of node can only be put as a direct child of the [`root`](/docs/structured-text/dast.md#root) node.

It does not allow children nodes.

```json
{
  "type": "block",
  "item": "1238455312"
}
```

### `thematicBreak`

A `thematicBreak` node represents a thematic break between paragraph-level elements: for example, a change of scene in a story, or a shift of topic within a section.

It does not allow children nodes.

```json
{
  "type": "thematicBreak"
}
```

---

# Working with Structured Text — Migrating content to Structured Text

Source [docs]: https://www.datocms.com/docs/structured-text/migrating-content-to-structured-text.md

The goal of this guide is to teach you how to migrate an existing DatoCMS project to [Structured Text](/docs/content-modelling/structured-text.md) fields. To illustrate the process, we'll use [an example project](https://dashboard.datocms.com/clone?id=42030&name=Structured+Text+demo) that you can clone on your account to follow each step.

> [!POSITIVE] In a hurry? Download the final result!
> If you prefer to skip the tutorial and just take a look at the final code, head over to this [GitHub repo](https://github.com/datocms/structured-text-migration-example).

## Setup

First of all, to follow this guide, make sure to clone this [example project](https://dashboard.datocms.com/clone?id=42030&name=Structured+Text+demo) into your own DatoCMS account.

Done? Great! Now's open the terminal, create a new directory for the migration project, and install the DatoCMS CLI:

Terminal window

```bash
mkdir structured-text-migrations
cd structured-text-migrations
npm init --yes
npm i --save-dev typescript datocms
tsc --init
mkdir -p migrations/utils
```

Now let's link the CLI to your DatoCMS project:

Terminal window

```bash
$ datocms link

✔ Choose a workspace › My organization
✔ Search and select a project › My project
✔ Directory where script migrations will be stored [./migrations]:
✔ API key of the DatoCMS model used to store migration data [schema_migration]:

Writing "datocms.config.json"... done
```

Once linked, the CLI will automatically resolve an API token for the linked project using your OAuth credentials. No need to manually create API tokens or set environment variables.

⚠️ **Important:** Make sure you do **not** commit this `.env` file to your version control system, as it contains sensitive credentials.

## High-level strategy & Project skeleton

This is the content schema of the cloned project:

(Image content)

The fields we want to convert into Structured Text are the following:

-   **HTML Article \> Content** (HTML multi-paragraph text);
-   **Markdown Article \> Content** (Markdown multi-paragraph text);
    
-   **Modular Content Article \> Content** (Modular content);
    

To do that, we're going to write three [migration scripts](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md) (one for each model) and test the result inside a [sandbox environment](/docs/scripting-migrations/introduction.md).

For every field, the high-level plan will be the same:

1.  Create a new Structured Text field for the model;
    
2.  For every article, take the old content, convert it to Structured Text and save it in the new field;
    
3.  Destroy the old field.
    

Inside the `migrations/utils` directory, we're adding some functions that we're going to use for all three migrations:

-   `createStructuredTextFieldFrom` creates a new Structured Text field with the same label and API key as an existing field, but prefixed with `structured_text_` (basically, step 1 of our plan);
-   `getAllRecords` fetches all the records of a specific model using the `nested` option, so that for modular content fields we get the full payload of the inner block records instead of just their ID (that's the first bit of step 2);
    
-   `swapFields` destroys the old field, and renames the new Structured Text field as the old one (that's step 3 of our plan);
    

Lastly, since:

-   some API calls expect the model ID and not the model API key, and
-   model IDs are different on each environment, and
    
-   we want our migrations to work on any environment
    

we can avoid hardcoding model IDs writing a `getModelIdsByApiKey` function that returns an object mapping API keys to model IDs:

./migrations/utils/createStructuredTextFieldFrom.ts

```typescript
import { Client, SimpleSchemaTypes } from 'datocms/lib/cma-client-node';

export default async function createStructuredTextFieldFrom(
  client: Client,
  modelApiKey: string,
  fieldApiKey: string,
  modelBlockIds: SimpleSchemaTypes.ItemTypeIdentity[],
): Promise<SimpleSchemaTypes.Field> {
  const legacyField = await client.fields.find(
    `${modelApiKey}::${fieldApiKey}`,
  );

  const newApiKey = `structured_text_${fieldApiKey}`;
  const label = `${legacyField.label} (Structured-text)`;

  console.log(`Creating ${modelApiKey}::${newApiKey}`);

  return client.fields.create(modelApiKey, {
    label,
    api_key: newApiKey,
    field_type: 'structured_text',
    fieldset: legacyField.fieldset,
    validators: {
      structured_text_blocks: {
        item_types: modelBlockIds,
      },
      structured_text_links: { item_types: [] },
    },
  });
}

// ./migrations/utils/getAllRecords.ts
import { Client } from 'datocms/lib/cma-client-node';

export default async function getAllRecords(
  client: Client,
  modelApiKey: string,
) {
  const records = await client.items.list({
    filter: { type: modelApiKey },
    nested: true,
  });
  console.log(`Found ${records.length} records!`);
  return records;
}

// ./migrations/utils/swapFields.ts
import { Client } from 'datocms/lib/cma-client-node';

export default async function swapFields(
  client: Client,
  modelApiKey: string,
  fieldApiKey: string,
) {
  const oldField = await client.fields.find(`${modelApiKey}::${fieldApiKey}`);
  const newField = await client.fields.find(
    `${modelApiKey}::structured_text_${fieldApiKey}`,
  );
  // destroy the old field
  await client.fields.destroy(oldField.id);
  // rename the new field
  await client.fields.update(newField.id, {
    api_key: fieldApiKey,
    label: oldField.label,
    position: oldField.position,
  });
}

// ./migrations/utils/getModelIdsByApiKey.ts
import { Client } from 'datocms/lib/cma-client-node';
import { ItemType } from '@datocms/cma-client/dist/types/generated/SimpleSchemaTypes';

export default async function getModelIdsByApiKey(
  client: Client,
): Promise<Record<string, ItemType>> {
  const models = await client.itemTypes.list();
  return models.reduce(
    (acc, itemType) => ({
      ...acc,
      [itemType.api_key]: itemType,
    }),
    {},
  );
}

// migrations/utils/findOrCreateUploadWithUrl.ts
import { Client } from 'datocms/lib/cma-client-node';
import path from 'path';

export default async function findOrCreateUploadWithUrl(
  client: Client,
  url: string,
) {
  let upload;

  if (url.startsWith('https://www.datocms-assets.com')) {
    const pattern = path.basename(url).replace(/^[0-9]+\-/, '');

    const matchingUploads = await client.uploads.list({
      filter: {
        fields: {
          filename: {
            matches: {
              pattern,
              case_sensitive: false,
              regexp: false,
            },
          },
        },
      },
    });

    upload = matchingUploads.find((u) => {
      return u.url === url;
    });
  }

  if (!upload) {
    upload = await client.uploads.createFromUrl({ url });
  }

  return upload;
}
```

## Migrating HTML content

Let's create the first migration script:

Terminal window

```bash
> datocms migrations:new convertHtmlArticles
Created migrations/1612281851_convertHtmlArticles.ts
```

Replace the content of the file with the following skeleton, which uses the utilities we just created:

./migrations/1612281851\_convertHtmlArticles.rs

```typescript
import getModelIdsByApiKey from './utils/getModelIdsByApiKey';
import createStructuredTextFieldFrom from './utils/createStructuredTextFieldFrom';
import htmlToStructuredText from './utils/htmlToStructuredText';
import getAllRecords from './utils/getAllRecords';
import swapFields from './utils/swapFields';
import convertImgsToBlocks from './utils/convertImgsToBlocks';
import { Client, SimpleSchemaTypes } from 'datocms/lib/cma-client-node';

type HtmlArticleType = SimpleSchemaTypes.Item & {
  title: string;
  content: string;
};

export default async function convertHtmlArticles(client: Client) {
  const modelIds = await getModelIdsByApiKey(client);

  await createStructuredTextFieldFrom(client, 'html_article', 'content', [
    modelIds.image_block.id,
  ]);

  const records = (await getAllRecords(
    client,
    'html_article',
  )) as HtmlArticleType[];

  for (const record of records) {
    const structuredTextContent = await htmlToStructuredText(
      record.content,
      convertImgsToBlocks(client, modelIds),
    );
    await client.items.update(record.id, {
      structured_text_content: structuredTextContent,
    });
    if (record.meta.status !== 'draft') {
      await client.items.publish(record.id);
    }
  }

  await swapFields(client, 'html_article', 'content');
}
```

A couple of notes:

-   Inside the HTML field there might be image tags (`<img />`). Structured Text does not have a specific node to handle images because it offers `block` nodes, which is a more powerful primitive. This means that, during the transformation process, we'll need to convert those `<img />` tags into block records of type "Image" (that's the same block currently used by the Modular Content field). For this reason, in line 19 we pass the `image_block` model ID to configure the newly created Structured Text field to accept such type of blocks;
-   In the highlighted lines we're going to perform the actual [records update](/docs/content-management-api/resources/item/create.md#structured-text-fields) and make sure we republish updated records (unless they were in draft).
    

So what is left to do is to implement the `htmlToStructuredText()` function.

The [`datocms-html-to-structured-text`](https://github.com/datocms/structured-text/tree/main/packages/html-to-structured-text) package offers a `parse5ToStructuredText` function that is meant to be used in NodeJS environments to perform the conversion from HTML to Structured Text ([`parse5`](https://github.com/inikulin/parse5) is a popular HTML parser for NodeJS).

Internally, the `parse5ToStructuredText` will take the parse5 Document, convert it into a [`hast`](https://github.com/syntax-tree/hast) tree, and then convert the `hast` tree into a [`dast`](/docs/structured-text/dast.md#datocms-abstract-syntax-tree--dast--specification) tree (that's the format of our Structured Text document). All these conversions might seem an overkill, but we will see later how having `hast` as an intermediate representation will come in handy.

Let's install some dependencies:

Terminal window

```bash
npm install --save-dev parse5 \
            datocms-html-to-structured-text \
            datocms-structured-text-utils \
            unist-utils-core@1.0.5
```

Now we have everything we need to build our `htmlToStructuredText` function:

./migrations/utils/htmlToStructuredText

```typescript
import { parse } from 'parse5';
import {
  parse5ToStructuredText,
  Options,
} from 'datocms-html-to-structured-text';
import { validate } from 'datocms-structured-text-utils';

export default async function htmlToStructuredText(
  html: string,
  settings: Options,
) {
  if (!html) {
    return null;
  }

  const result = await parse5ToStructuredText(
    parse(html, {
      sourceCodeLocationInfo: true,
    }),
    settings,
  );

  const validationResult = validate(result);

  if (!validationResult.valid) {
    throw new Error(validationResult.message);
  }

  return result;
}
```

Please note that in the highlighted line we use the `validate` function from the `datocms-structured-text-utils` package to make sure that the final result is valid Structured Text.

#### Converting image tags into blocks

The code above will convert 99% of the HTML correctly, but **images present in the content will be skipped**.

As we already noted before, that's because Structured Text does not have a specific node to handle images. Instead, it offers [`block` nodes](/docs/structured-text/dast.md#block), which can handle images and much more. We have to pass some additional settings to the `parse5ToStructuredText` function to tell it how to convert `<img />` tags to `block` nodes:

./migrations/utils/convertImgsToBlocks.ts

```typescript
import {
  buildBlockRecord,
  Client,
  SimpleSchemaTypes,
} from 'datocms/lib/cma-client-node';
import { visit, find } from 'unist-utils-core';
import {
  HastNode,
  HastElementNode,
  CreateNodeFunction,
  Context,
} from 'datocms-html-to-structured-text';
import { Options } from 'datocms-html-to-structured-text';
import findOrCreateUploadWithUrl from './findOrCreateUploadWithUrl';

export default function convertImgsToBlocks(
  client: Client,
  modelIds: Record<string, SimpleSchemaTypes.ItemType>,
): Options {
  return {
    preprocess: (tree: HastNode) => {
      const liftedImages = new WeakSet();

      const body = find(
        tree,
        (node: HastNode) =>
          (node.type === 'element' && node.tagName === 'body') ||
          node.type === 'root',
      );

      visit<HastNode, HastElementNode & { children: HastNode[] }>(
        body,
        (node, index, parents) => {
          if (
            node.type !== 'element' ||
            node.tagName !== 'img' ||
            liftedImages.has(node) ||
            parents.length === 1
          ) {
            return;
          }

          const imgParent = parents[parents.length - 1];
          imgParent.children.splice(index, 1);

          let i = parents.length;
          let splitChildrenIndex = index;
          let childrenAfterSplitPoint: HastNode[] = [];

          while (--i > 0) {
            const parent = parents[i];
            const parentsParent = parents[i - 1];

            childrenAfterSplitPoint =
              parent.children.splice(splitChildrenIndex);
            splitChildrenIndex = parentsParent.children.indexOf(parent);

            let nodeInserted = false;

            if (i === 1) {
              splitChildrenIndex += 1;
              parentsParent.children.splice(splitChildrenIndex, 0, node);
              liftedImages.add(node);

              nodeInserted = true;
            }

            splitChildrenIndex += 1;

            if (childrenAfterSplitPoint.length > 0) {
              parentsParent.children.splice(splitChildrenIndex, 0, {
                ...parent,
                children: childrenAfterSplitPoint,
              });
            }

            if (parent.children.length === 0) {
              splitChildrenIndex -= 1;
              parentsParent.children.splice(
                nodeInserted ? splitChildrenIndex - 1 : splitChildrenIndex,
                1,
              );
            }
          }
        },
      );
    },
    // now that images are top-level, convert them into `block` dast nodes
    handlers: {
      img: async (
        createNode: CreateNodeFunction,
        node: HastNode,
        _context: Context,
      ) => {
        if (node.type !== 'element' || !node.properties) {
          return;
        }

        const { src: url } = node.properties;
        const upload = await findOrCreateUploadWithUrl(client, url);

        return createNode('block', {
          item: buildBlockRecord({
            item_type: { id: modelIds.image_block.id, type: 'item_type' },
            image: {
              upload_id: upload.id,
            },
          }),
        });
      },
    },
  };
}
```

A couple notes:

-   We use the `handlers` option to specify how to convert the `<img />` `hast` nodes tags to [`dast` `block` nodes](/docs/structured-text/dast.md#block) (the default behavior, as we saw, is to simply skip them);
-   The `block` node should contain a block record of type Image (that's the same block currently used by the Modular Content field), which in turn has a single-asset `image` field. In line 79 we create a new asset starting from the `src` tag of the image, to feed it to the `image` field. Luckily, the handlers are async functions, so we can easily perform an asyncronous operation inside of it.
    
-   Since in the `dast` format, a `block` node can only be at root level, we use the `preprocess` option to tweak the `hast` tree and lift every image node up to the root (in case they're inside paragraphs or other tags).
    

We can [test the migration](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md) with the following command from the Terminal, which will clone the primary environment into a sandbox, and run the migration:

Terminal window

```bash
npx datocms migrations:run --destination=with-structured-text
✔ Running 1612281851_convertHtmlArticles.ts...
Done!
```

Success! The article content is correctly converted to structured text.

## Migrating Markdown content

Once we know how to perform the HTML-to-Structured-Text conversion, we only have to do some minor changes to make it work also for Markdown content.

As we just saw, the `datocms-html-to-structured-text` package knows how to convert an [`hast`](https://github.com/syntax-tree/hast) tree (HTML) to a [`dast`](/docs/structured-text/dast.md#datocms-abstract-syntax-tree--dast--specification) tree (Structured Text), so if we can convert a Markdown string to `hast`, then the rest of the code will be basically the same.

Luckily, `hast` is part of the [unified](https://github.com/unifiedjs/unified) ecosystem, which also includes:

-   an analogue specification for representing Markdown in a syntax tree called [`mdast`](https://github.com/syntax-tree/mdast);
-   a tool to convert Markdown strings to `mdast`;
    
-   a tool to convert `mdast` trees to `hast`.
    

Let's install all the packages we need:

Terminal window

```bash
npm install --save-dev unified@9 remark-parse@9 mdast-util-to-hast@10
```

We can now create a function similar to `htmlToStructuredText` called `markdownToStructuredText` that connects all the dots:

./migrations/utils/markdownToStructuredText.ts

```typescript
import unified from 'unified';
import toHast from 'mdast-util-to-hast';
import parse from 'remark-parse';
import {
  hastToStructuredText,
  Options,
  HastRootNode,
} from 'datocms-html-to-structured-text';
import { validate } from 'datocms-structured-text-utils';

export default async function markdownToStructuredText(
  markdown: string,
  options: Options,
) {
  if (!markdown) {
    return null;
  }

  const mdastTree = unified().use(parse).parse(markdown);
  const hastTree = toHast(mdastTree) as HastRootNode;
  const result = await hastToStructuredText(hastTree, options);

  const validationResult = validate(result);

  if (!validationResult.valid) {
    throw new Error(validationResult.message);
  }

  return result;
}
```

We can now create a new migration script:

Terminal window

```bash
> datocms migrations:new convertMarkdownArticles
Created migrations/1612340785_convertMarkdownArticles.ts
```

And basically copy the previous migration, just replacing the name of the model (from `html_article` to `markdown_article`), and the call to `htmlToStructuredText` with a call to `markdownToStructuredText`:

./migrations/1612340785\_convertMarkdownArticles.ts

```typescript
import getModelIdsByApiKey from './utils/getModelIdsByApiKey';
import createStructuredTextFieldFrom from './utils/createStructuredTextFieldFrom';
import markdownToStructuredText from './utils/markdownToStructuredText';
import convertImgsToBlocks from './utils/convertImgsToBlocks';
import getAllRecords from './utils/getAllRecords';
import swapFields from './utils/swapFields';
import { Client, SimpleSchemaTypes } from 'datocms/lib/cma-client-node';

type MdArticleType = SimpleSchemaTypes.Item & {
  title: string;
  content: string;
};
export default async function (client: Client) {
  const modelIds = await getModelIdsByApiKey(client);

  await createStructuredTextFieldFrom(client, 'markdown_article', 'content', [
    modelIds.image_block.id,
  ]);

  const records = (await getAllRecords(
    client,
    'markdown_article',
  )) as MdArticleType[];

  for (const record of records) {
    const structuredTextContent = await markdownToStructuredText(
      record.content,
      convertImgsToBlocks(client, modelIds),
    );
    await client.items.update(record.id, {
      structured_text_content: structuredTextContent,
    });
    if (record.meta.status !== 'draft') {
      await client.items.publish(record.id);
    }
  }

  await swapFields(client, 'markdown_article', 'content');
}
```

We can now run the new migration inside the sandbox environment we already created for the first migration:

Terminal window

```bash
> datocms migrations:run --source=with-structured-text --in-place
✔ Running 1612340785_convertMarkdownArticles.ts...
Done!
```

## Migrating Modular Content fields

To migrate Modular Content fields into Structured Text fields, we must acknowledge the fact that both fields allow nested record blocks: the difference between the two is that Modular Content is basically an array of record blocks, while in Structed Text record blocks are inside the `dast` tree in [nodes of type `block`](/docs/structured-text/dast.md#block). In other words, our task here is, for every modular content, to transform an array of block records into a single `dast` document. It's up to us to decide how to convert each block we encounter into one/many nodes into our `dast` document.

Let's take a look at the project schema again:

(Image content)

The existing Modular Content field supports three block types:

-   Text (which in turn contains a `text` Markdown field);
-   Code (which has two fields, one that contains the actual code and another that stores the language);
    
-   Image (which, as we already know, it contains a single-asset field called `image`).
    

Here's the code for our migration:

./migrations/1612340785\_convertModularArticles.ts

```typescript
import { Document, Node, validate } from 'datocms-structured-text-utils';
import getModelIdsByApiKey from './utils/getModelIdsByApiKey';
import createStructuredTextFieldFrom from './utils/createStructuredTextFieldFrom';
import getAllRecords from './utils/getAllRecords';
import swapFields from './utils/swapFields';
import markdownToStructuredText from './utils/markdownToStructuredText';
import convertImgsToBlocks from './utils/convertImgsToBlocks';
import { Client, SimpleSchemaTypes } from 'datocms/lib/cma-client-node';

type ModularArticleType = SimpleSchemaTypes.Item & {
  title: string;
  content: any;
};

export default async function (client: Client) {
  const modelIds = await getModelIdsByApiKey(client);

  await createStructuredTextFieldFrom(
    client,
    'modular_content_article',
    'content',
    [modelIds.image_block.id, modelIds.text_block.id, modelIds.code_block.id],
  );

  const records = (await getAllRecords(
    client,
    'modular_content_article',
  )) as ModularArticleType[];

  for (const record of records) {
    const rootNode = {
      type: 'root',
      children: [] as Node[],
    };

    for (const block of record.content) {
      switch (block.relationships.item_type.id) {
        case modelIds.text_block.id: {
          const markdownSt = await markdownToStructuredText(
            block.text,
            convertImgsToBlocks(client, modelIds),
          );

          if (markdownSt) {
            rootNode.children = [
              ...rootNode.children,
              ...markdownSt.document.children,
            ];
          }
          break;
        }

        case modelIds.code_block.id: {
          rootNode.children.push({
            type: 'code',
            language: block.language,
            code: block.code,
          });
          break;
        }
        default: {
          delete block.id;
          delete block.meta;
          delete block.createdAt;
          delete block.updatedAt;

          rootNode.children.push({
            type: 'block',
            item: block,
          });
          break;
        }
      }
    }

    const result = {
      schema: 'dast',
      document: rootNode,
    } as Document;

    const validationResult = validate(result);

    if (!validationResult.valid) {
      throw new Error(validationResult.message);
    }

    await client.items.update(record.id, {
      structured_text_content: result,
    });

    if (record.meta.status !== 'draft') {
      await client.items.publish(record.id);
    }
  }

  await swapFields(client, 'modular_content_article', 'content');
}
```

Every time we need to convert a Modular Content field, we start by creating an empty Dast `root` node (that is, one with no children, line 33).

Then, for every block contained in the modular content (line 38), we're going to accumulate children inside the `root` node:

-   If it is a Text block (line 40), we use the `markdownToStructuredText` function to convert its Markdown content into a Dast tree, then take the children of the resulting `root` node and add them to our accumulator;
-   Since Dast supports [nodes of type `code`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/types.ts#L54-L59), if we encounter a Code block (line 55), we simply convert it to `code` node, and add it to the accumulator;
    
-   If we find an Image block (line 63), we'll wrap the block into a Dast `block` node, and add it to the accumulator as it is.
    

## Wrapping up

Once you get to know the [Structured Text](/docs/content-modelling/structured-text.md#structured-text-on-the-api) format, it becomes quite straightforward converting from/to its Dast tree representation of nodes, and the DatoCMS API, coupled with migrations/sandbox environments, makes it easy to perform any kind of treatment to your content.

You can download the final code from this [GitHub repo](https://github.com/datocms/structured-text-migration-example).

---

# Plugin SDK — Introduction to the DatoCMS Plugin SDK

Source [docs]: https://www.datocms.com/docs/plugin-sdk/introduction.md

Although DatoCMS offers a wide range of features and configurations by default, with **plugins** it is possible to take a further leap forward. You can integrate third-party services with our platform or build custom integrations tailored specifically to your business and user needs.

### What is a DatoCMS Plugin?

Technically speaking, DatoCMS plugins are small web apps that run in a sandboxed `<iframe>` inside our UI and interact with the main DatoCMS app through the Plugin SDK. They can be implemented with basic HTML and JavaScript, or using more advanced client-side frameworks such as React, Angular or Vue.

> [!POSITIVE] Pro tip
> If you're using React, you can take advantage of the [`datocms-react-ui` package](/docs/plugin-sdk/react-datocms-ui.md) that provides a set of ready-to-use components that are consistent with the UI of the main DatoCMS application.

### What can plugins do?

> [!PROTIP] Pro tip: Example plugins built by the community
> Before you build your own plugin, you might want to see if similar functionality is already available in our Community Plugins Marketplace: [https://www.datocms.com/marketplace/plugins](https://www.datocms.com/marketplace/plugins.md)

A huge variety of enhancements to the DatoCMS web app are possible. From small field editor improvements to deeply-integrated full-page applications, plugins make customizing the DatoCMS interface effortless.

Some common use cases are:

-   adding custom field editors to improve the editor experience;
-   managing content versions for running A/B tests on structured content using personalization tools;
    
-   tailoring the default entry editor to suit your specific needs;
-   seamlessly integrating DatoCMS with third-party software and services.
    

For some real-world examples, you can take a look at our [Marketplace](https://www.datocms.com/marketplace/plugins.md), which already offers 100+ open-source plugins.

### How plugins work

The way in which plugins modify the default DatoCMS interface is through the concept of **hooks**.

The SDK provides a set of locations where plugins can intervene by adding functionality (ie. custom pages, sidebar panels, etc.), and for each of these locations a set of hooks are provided.

Hooks serve three main purposes:

-   **Declare the plugin intentions** (e.g., "I want to add a tab in the top navigation bar of DatoCMS that points to a custom page X").
-   **Render the content for the** `**iframe**` associated with the declared custom locations (e.g., "when the user enters custom page X, let me render my stuff")
    
-   **Intercept specific events** happening on the interface, and execute custom code, or change the way the regular interface behaves.
    

You can read in detail about all the hooks and locations provided in the following sections of the guide.

### Distribution: private vs public plugins

As we'll learn in the following sections, plugins can either be private, or publicly released into the Marketplace.

A private plugin is built by you for your specific organization's needs to optimize your organization's editorial experience. It is fully under your control and not accessible by other organizations.

If you think a plugin you've made would be useful to other community members, then we strongly encourage its release in our public [Marketplace](https://www.datocms.com/marketplace/plugins.md). Everyone can contribute new plugins to the marketplace by releasing them as NPM packages.

#### Learn more about plugins

Check out this tutorial on how to make the most out of the plugins in our Marketplace, or how to build your own:

[

(Image content)

Intro to the Plugin Ecosystem

Play video »

](https://www.datocms.com/user-guides/the-basics/intro-to-the-plugin-ecosystem.md)

[

(Image content)

How to start developing plugins for DatoCMS

Play video »

](https://youtu.be/sc8sm34tyWw)

[

(Image content)

Exploring DatoCMS Plugins that help authors

Play video »

](https://youtu.be/PDLCgSFjrac)

---

# Plugin SDK — Build your first DatoCMS plugin

Source [docs]: https://www.datocms.com/docs/plugin-sdk/build-your-first-plugin.md

The demo plugin we're about to build is called **Record Metrics**. It will enhance your editorial experience when creating for instance a blog post by providing useful metrics such as word count and a reading time indicator. The metrics will be shown in a sidebar panel for a given record.

# Prerequisites

In order to successfully complete this tutorial:

-   You'll need the latest LTS version of [Node.js](https://nodejs.org/en/) installed on your machine. If you don't have `Node.js` installed, you can download it [here](https://nodejs.org/en/download/).
-   You should be comfortable using your computer’s command line and text editor.
    
-   You’ll need to be able to read and write HTML, CSS, and JavaScript.
-   You should be familiar with installing software using [NPM](https://www.npmjs.com/).
    
-   You'll need a free DatoCMS account and a project. You can sign up [here](https://dashboard.datocms.com/signup).
-   You'll need either Firefox or Chrome. Safari currently does not work due to a limitation in how it handles insecure iframes pointing to `localhost`.
    

# Tools

We will use several tools and libraries throughout the tutorial. We chose these technologies because we think they provide the best possible developer experience.

###### React

We use [React](https://reactjs.org/) to render our views for the app and handle our logic. React is a JavaScript library for building user interfaces. However, using React is not mandatory to create apps.

###### Plugin SDK

The Plugin SDK provides the methods that are necessary to interact with the DatoCMS web app. We will only use a subset of the methods, but if you want to know the full scope of what is possible, take a look at the other sections of this guide.

###### DatoCMS React UI

To achieve the same look and feel of the DatoCMS web app we use `datocms-react-ui` which exposes a number of React components ready to be used.

###### TypeScript

The plugin is written in TypeScript. This allows us to have documentation, autocompletion in our editor, as well as the assurance that we are passing the right parameters to our libraries. However, you do not need any TypeScript knowledge in order to complete this tutorial. Plugins can also be written in JavaScript without losing any of the functionality.

# Set up your project

As a first step, you need to scaffold the project. We will use a tool called [`tmplr`](https://github.com/loreanvictor/tmplr) to prepare a Vite-powered plugin template:

Terminal window

```bash
npx tmplr datocms/datocms-plugin-template --dir my-first-plugin
```

Follow the prompts, then navigate to the newly created folder and start the app:

Terminal window

```bash
cd my-first-plugin
npm install && npm run dev
```

This hosts your plugin on `http://localhost:5173`. We'll later connect to this through the DatoCMS web app.

# Install your plugin in the DatoCMS web app

In order for you to see your app running in the DatoCMS web app, you need to create a private plugin in DatoCMS.

> [!POSITIVE] Plugins are private unless you choose to publish them
> DatoCMS plugins are private by default (only accessible in the project you installed it in) unless you [choose to publish it to the public plugin marketplace](/docs/plugin-sdk/publishing-to-marketplace.md), which would make it accessible to all DatoCMS customers.

#### Create your private Plugin

Enter your project, and go to **Configuration** **\>** **Plugins**. Click on "**Add a new private plugin**":

(Video content)

In the modal, provide details about your plugin:

-   Provide a name and (optionally) a description for your plugin. This can be whatever you want; we chose **Record Metrics** for this tutorial.
-   Enter the *Entry point URL*. This is the URL where our app is running. Since we are running our app locally during development, the URL is `http://localhost:5173`. (Later, once you [deploy](/docs/plugin-sdk/build-your-first-plugin.md#deployment) your plugin, you can change the entry point to another location.)
    
-   Specify any [special permission](/docs/plugin-sdk/additional-permissions.md) you want to grant to the plugin. For this tutorial, we don't need any of them.
    

Then submit the form to create the plugin. Congrats, your plugin is now installed in your current project and environment! 🎉

Once you're done with local development, you'll probably want to [deploy your plugin](/docs/plugin-sdk/build-your-first-plugin.md#deployment) so it can be accessed by your team members without needing to run your local development server.

(Video content)

# Configure your plugin

The [config screen](/docs/plugin-sdk/config-screen.md) of the plugin is rendered by the `ConfigScreen` React component.

Let's fire up our code editor of choice and open the `src/entrypoints/ConfigScreen.tsx` file in the project directory that was previously generated. Any changes you make here will be reflected in the DatoCMS web app. Let's change our welcome text from `Welcome to your plugin!` to `Welcome to Record Metrics!`

Save the file and watch the config screen update in real time:

(Video content)

> [!PROTIP] Pro tip: Use <ContextInspector />
> Inside the `src/entrypoints/ConfigScreen.tsx` file you'll notice the use of `<ContextInspector />` , which is a component made available by `datocms-react-ui` to get an instant overview of all the information/methods available within any SDK hook.
> 
> Remember to use it during development, it's very convenient to avoid going back and forth in the documentation!

# Add the sidebar panel

To add [sidebar panels](/docs/plugin-sdk/sidebar-panels.md) to the DatoCMS interface, we need to implement the `itemFormSidebarPanels` and `renderItemFormSidebarPanel` hooks.

Open the `src/index.tsx` file and add the following code:

```tsx
import SidebarMetrics from './entrypoints/SidebarMetrics';

connect({
  // ...
  itemFormSidebarPanels() {
    return [
      {
        id: 'metrics',
        label: 'Metrics',
      },
    ];
  },
  renderItemFormSidebarPanel(sidebarPaneId, ctx) {
    render(<SidebarMetrics ctx={ctx} />);
  },
});
```

We also need to add the new `SidebarMetrics` component in `src/entrypoints/SidebarMetrics.tsx`:

```tsx
import { RenderItemFormSidebarPanelCtx } from 'datocms-plugin-sdk';
import { Canvas } from 'datocms-react-ui';

type PropTypes = {
  ctx: RenderItemFormSidebarPanelCtx;
};

export default function SidebarMetrics({ ctx }: PropTypes) {
  return <Canvas ctx={ctx}>Hello from the sidebar!</Canvas>;
}
```

Make sure to have a model with some text fields, and a record we can test the plugin on (if you are not familiar with the concept of models, you can read more about them [here](/docs/general-concepts/data-modelling.md)).

Then go to your Content tab, and create a blog post record. You should see a "Metrics" sidebar panel now on the page:

(Video content)

All the changes you make here to the component will also be reflected directly in the web app.

## Calculate the metrics

It's time to calculate some metrics for the record. For calculating the word count and the reading time we will use a library called [`reading-time`](https://github.com/michael-lynch/reading-time). Navigate to your project folder and install the libraries and its dependencies with:

Terminal window

```bash
npm install reading-time
npm install stream util --save-dev
```

We can use the `ctx` object, which gets passed into every hook, to interact with DatoCMS:

-   The `ctx.fields` object holds all the currently loaded fields for the current project;
-   The `ctx.itemType` object holds the model for the current record;
    
-   The `ctx.formValues` object holds the current values present in the record form;
    

We can use this information to get the values of all the text fields present in the record, concatenate them in a single string and then call the `readingTime` function, which will calculate our desired metrics. It will do all the heavy lifting for us and return an object which holds the word count and the time to read.

The last thing we want to do is display the calculated metrics in our sidebar. For this we import the `Canvas` component from `datocms-react-ui` to give our app the look and feel of the DatoCMS web app.

The final code should look like this:

```tsx
import { RenderItemFormSidebarPanelCtx } from 'datocms-plugin-sdk';
import { Canvas } from 'datocms-react-ui';
import readingTime from 'reading-time';
import { Field } from 'datocms-plugin-sdk';

type PropTypes = {
  ctx: RenderItemFormSidebarPanelCtx;
};

export default function SidebarMetrics({ ctx }: PropTypes) {
  const modelFields = ctx.itemType.relationships.fields.data
    .map((link) => ctx.fields[link.id])
    .filter<Field>((x): x is Field => !!x);

  const textFields = modelFields.filter((field) =>
    ['text', 'string'].includes(field.attributes.field_type),
  );

  const allText = textFields
    .map((field) => ctx.formValues[field.attributes.api_key])
    .join(' ');

  const stats = readingTime(allText || '');

  return (
    <Canvas ctx={ctx}>
      <ul>
        <li>Word count: {stats.words}</li>
        <li>Reading time: {stats.text}</li>
      </ul>
    </Canvas>
  );
}
```

Type some content in your field and see how the app updates!

(Video content)

## Deployment

To deploy your plugin and make it available to everyone in your organization, you need to create a production build of your app and then host it somewhere on the internet. We strongly suggest using Netlify or Vercel, as they make the overall experience incredibly easy.

When configuring your hosting service, make sure to specify the following build command:

Terminal window

```bash
npm run build
```

Once deployed, go to "Project Settings \> Plugins", and inside your plugin click the "Edit private plugin" button. In the modal, change the "Entry point URL" to the new Netlify/Vercel URL.

Congratulations, you just deployed your first plugin! 🥳

#### Build a Plugin Video tutorial

Learn to build a DatoCMS plugin from scratch with this video tutorial:

[

(Image content)

How to start developing plugins for DatoCMS

Play video »

](https://youtu.be/sc8sm34tyWw)

---

# Plugin SDK — Real-world examples

Source [docs]: https://www.datocms.com/docs/plugin-sdk/real-world-examples.md

To understand how all the pieces fit together, many developers find useful to read the complete source code of an already published plugin.

Luckily, most of the plugins published in the [Marketplace](https://www.datocms.com/marketplace/plugins.md) are 100% open source: you can easily open their GitHub repository and inspect their code from the “Visit homepage” button present in their details page:

(Image content)

> [!WARNING] Be careful what you read!
> Be careful, because some of the plugins in the Marketplace may have been built using a legacy version of the SDK, **so they might not be a good example to follow!**
> 
> Always check in the `package.json` that they're requiring the `datocms-plugin-sdk` NPM package, and not the legacy one (which is called `datocms-plugins-sdk`, with `plugins` in plural form).

## Always up-to-date official plugins

We personally take care of keeping a number of plugins in the Marketplace up to date, so you can always be sure they run on the most up-to-date version of the SDK. It might be a good idea to start studying with one of them!

They are all stored in a single GitHub monorepo:

💻 **Official plugins monorepo:** [https://github.com/datocms/plugins](https://github.com/datocms/plugins)

If you'd like to have more examples, don't be afraid to ask, we are here to help you!

[

(Image content)

Intro to the Plugin Ecosystem

Play video »

](https://www.datocms.com/user-guides/the-basics/intro-to-the-plugin-ecosystem.md)

[

(Image content)

Exploring DatoCMS Plugins that help authors

Play video »

](https://youtu.be/PDLCgSFjrac)

---

# Plugin SDK — What hooks are

Source [docs]: https://www.datocms.com/docs/plugin-sdk/what-hooks-are.md

Hooks are nothing but named JS functions that plugins can implement within their code.

A number of different hooks are made available by the SDK, each with a specific purpose and function. **By implementing hooks, plugins can add functionalities or tweak the interface** of a project in a controlled and safe way.

### What can plugins do?

You can read in detail about all the hooks in the following sections of the guide, but to give an overall view, a plugin can implement hooks to:

-   [Manage their config screen and user settings](/docs/plugin-sdk/config-screen.md)
-   [Render custom pages and link them from the DatoCMS navigation bars](/docs/plugin-sdk/custom-pages.md)
    
-   [Show custom sidebar panels when editing a record](/docs/plugin-sdk/sidebar-panels.md)
-   [Tweak/enhance the way fields can be edited](/docs/plugin-sdk/field-extensions.md)
    
-   [Open custom modals](/docs/plugin-sdk/modals.md)
-   [Intercept specific events happening on the interface, and execute custom code, or change the way the regular interface behaves.](/docs/plugin-sdk/event-hooks.md)
    

Other hooks will be made available in future versions of the SDK, to let plugins intervene in other places of the DatoCMS interface.

---

# Plugin SDK — Config screen

Source [docs]: https://www.datocms.com/docs/plugin-sdk/config-screen.md

Quite often, a plugin needs to offer a set of configuration options to the user who installs it.

DatoCMS offers every plugin a configuration screen and a **read-write object that can be used to store such settings**. It is a free-form object, with no restrictions in the format. Plugins can store what they want in it, and retrieve its value anytime they need in any hook.

As the configuration parameters are completely arbitrary, **it is up to the plugin itself to show the user a form** through which they can be changed.

The hook provided for this purpose is called [`renderConfigScreen`](/docs/plugin-sdk/config-screen.md#renderConfigScreen), and it will be called by DatoCMS when the user visits the details page of the installed plugin:

(Image content)

#### Implementing a simple configuration form

To give a very simple example, let's say our plugin wants to provide the end user with a simple boolean flag called `debugMode`. If this flag is enabled, then the plugin will display a series of debug messages in the console as it works.

The first step is to implement the [`renderConfigScreen`](/docs/plugin-sdk/config-screen.md#renderConfigScreen) hook, which will simply initialize React by rendering a custom component called `ConfigScreen`:

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, RenderConfigScreenCtx } from 'datocms-plugin-sdk';

connect({
  renderConfigScreen(ctx: RenderConfigScreenCtx) {
    ReactDOM.render(
      <React.StrictMode>
        <ConfigScreen ctx={ctx} />
      </React.StrictMode>,
      document.getElementById('root'),
    );
  },
});
```

The hook, in its `ctx` argument, provides a series of information and methods for interacting with the main application, and for now we'll just pass the whole object to the component, in the form of a React prop:

```tsx
import { Canvas } from 'datocms-react-ui';

type PropTypes = {
  ctx: RenderConfigScreenCtx;
};

function ConfigScreen({ ctx }: PropTypes) {
  return (
    <Canvas ctx={ctx}>
      Hello from the config screen!
    </Canvas>
  );
}
```

> [!WARNING] Always use the canvas!
> It is important to wrap the content inside the `Canvas` component, so that the iframe will continuously auto-adjust its size based on the content we're rendering, and to give our app the look and feel of the DatoCMS web app.

It is now time to setup our form:

```tsx
import { Canvas, SwitchField } from 'datocms-react-ui';

// configuration object starts as an empty object
type FreshInstallationParameters = {};

// this is how we want to save our settings
type ValidParameters = { devMode: boolean };

// parameters can be either empty or filled in
type Parameters = FreshInstallationParameters | ValidParameters;

export default function ConfigScreen({ ctx }: PropTypes) {
  const parameters = ctx.plugin.attributes.parameters as Parameters;

  return (
    <Canvas ctx={ctx}>
      <SwitchField
        id="01"
        name="development"
        label="Enable development mode?"
        hint="Log debug information in console"
        value={parameters.devMode}
        onChange={(newValue) => {
          ctx.updatePluginParameters({ devMode: newValue });
          ctx.notice('Settings updated successfully!');
        }}
      />
    </Canvas>
  );
}
```

The important things to notice are that:

-   we can access the currently saved configuration object through `ctx.plugin.attributes.parameters`
-   we can call `ctx.updatePluginParameters()` to save a new configuration object.
    

Once saved, settings are always available as `ctx.plugin.attributes.parameters` in any of the other hooks, so that your plugin can have different behaviours based on them.

> [!NOTE] Parameters are updated in real-time
> When the configuration form is saved, the parameters are persisted and propagated in real-time to all other users who are looking at the same form.

### Using a form management library

If you have more complex settings, feel free to use one of the many form management libraries available for React to avoid code repetition.

We recommend [react-final-form](https://github.com/final-form/react-final-form), as it works well and is quite lightweight (~8kb). Here's a more complete example using it:

```tsx
import { RenderConfigScreenCtx } from 'datocms-plugin-sdk';
import {
  Button,
  Canvas,
  SwitchField,
  TextField,
  Form,
  FieldGroup,
} from 'datocms-react-ui';
import { Form as FormHandler, Field } from 'react-final-form';

type PropTypes = {
  ctx: RenderConfigScreenCtx;
};

type FirstInstallationParameters = {};
type ValidParameters = { devMode: boolean; title: string };
type Parameters = FirstInstallationParameters | ValidParameters;

export default function ConfigScreen({ ctx }: PropTypes) {
  return (
    <Canvas ctx={ctx}>
      <FormHandler<Parameters>
        initialValues={ctx.plugin.attributes.parameters}
        validate={(values) => {
          const errors: Record<string, string> = {};

          if (!values.title) {
            errors.title = 'This field is required!';
          }
          return errors;
        }}
        onSubmit={async (values) => {
          await ctx.updatePluginParameters(values);
          ctx.notice('Settings updated successfully!');
        }}
      >
        {({ handleSubmit, submitting, dirty }) => (
          <Form onSubmit={handleSubmit}>
            <FieldGroup>
              <Field name="title">
                {({ input, meta: { error } }) => (
                  <TextField
                    id="title"
                    label="Title"
                    hint="Title to show"
                    placeholder="Your title"
                    required
                    error={error}
                    {...input}
                  />
                )}
              </Field>
              <Field name="devMode">
                {({ input, meta: { error } }) => (
                  <SwitchField
                    id="devMode"
                    label="Enable development mode?"
                    hint="Log debug information in console"
                    error={error}
                    {...input}
                  />
                )}
              </Field>
            </FieldGroup>
            <Button
              type="submit"
              fullWidth
              buttonSize="l"
              buttonType="primary"
              disabled={submitting || !dirty}
            >
              Save settings
            </Button>
          </Form>
        )}
      </FormHandler>
    </Canvas>
  );
}
```

This will be the final result:

(Image content)

#### `renderConfigScreen(ctx)`

This function will be called when the plugin needs to render the plugin's configuration form.

##### Context object

The following properties and methods are available in the `ctx` argument:

---

# Plugin SDK — Custom pages

Source [docs]: https://www.datocms.com/docs/plugin-sdk/custom-pages.md

Through plugins it is possible to enrich the functionalities of DatoCMS by adding new pages and sections to the standard interface. These pages are almost full-screen, **100% customisable**, and the end-user can reach them through links/menu items that can be added to the different DatoCMS navigation menus.

For example, the [Custom Page](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-custom-page.md) plugin lets you embed any external URL inside DatoCMS, while the [Content Calendar](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-content-calendar.md) plugin uses a custom page to explore your records inside a calendar:

(Video content)

A page is nothing more than an iframe, inside of which the plugin developer can render what they prefer, while also having the possibility to:

-   access a series of information related to the project in which the plugin is installed or the logged-in user;
-   make calls to DatoCMS to produce various effects and interacting with the main application (ie. navigate to other pages, trigger notifications, opening modals, etc.);
    

### Adding a link to the custom page

The SDK provides a number of hooks for adding links to custom pages within the navigation menus of DatoCMS.

#### Top-bar navigation items

(Image content)

To add one or more tabs to the top bar of the interface, you can use the [`mainNavigationTabs`](/docs/plugin-sdk/custom-pages.md#mainNavigationTabs) hook:

```typescript
import { connect, MainNavigationTabsCtx } from 'datocms-plugin-sdk';

connect({
  mainNavigationTabs(ctx: MainNavigationTabsCtx) {
    return [
      {
        label: 'Analytics',
        icon: 'analytics',
        pointsTo: {
          pageId: 'analytics',
        },
      },
    ];
  },
});
```

The `pageId` property is crucial here, as it specifies which custom page you want to display when you click the tab. If you wish, you can also customize the insertion point of the menu item via the `placement` property:

```typescript
{
  // ...other properties
  placement: ['before', 'content'],
}
```

In this case, we are asking to show the tab before the default "Content" tab.

As for the `icon`, you can either use one of the [Awesome 5 Pro solid icons](https://fontawesome.com/v5/search?o=r&s=solid) by their name, explicitly pass a custom SVG or use an emoji:

```javascript
icon: {
  type: 'svg',
  viewBox: '0 0 448 512',
  content:
    '<path fill="currentColor" d="M448,230.17V480H0V230.17H141.13V355.09H306.87V230.17ZM306.87,32H141.13V156.91H306.87Z" class=""></path>',
}
```

```javascript
icon: {
  type: "emoji",
  emoji: "🎉"
}
```

#### Menu item in the Content navigation sidebar

(Image content)

Similarly, we can use the [`contentAreaSidebarItems`](/docs/plugin-sdk/custom-pages.md#contentAreaSidebarItems) hook to add menu items to the sidebar that is displayed when we are inside the "Content" area:

```typescript
import { connect, ContentAreaSidebarItemsCtx } from 'datocms-plugin-sdk';

connect({
  contentAreaSidebarItems(ctx: ContentAreaSidebarItemsCtx) {
    return [
      {
        label: 'Welcome!',
        icon: 'igloo',
        placement: ['before', 'menuItems'],
        pointsTo: {
          pageId: 'welcome',
        },
      },
    ];
  },
});
```

This code will add a menu item above the default menu items present in the sidebar.

#### Custom section in the Settings area

It is also possible to add new sections in the sidebar present in the "Settings" area with the [`settingsAreaSidebarItemGroups`](/docs/plugin-sdk/custom-pages.md#settingsAreaSidebarItemGroups) hook:

```typescript
import { connect, SettingsAreaSidebarItemGroupsCtx } from 'datocms-plugin-sdk';

const labels: Record<string, string> = {
  "en": 'Settings',
  "it": 'Impostazioni',
  "es": 'Configuración',
};

connect({
  settingsAreaSidebarItemGroups(ctx: SettingsAreaSidebarItemGroupsCtx) {
    if (!ctx.currentRole.attributes.can_edit_schema) {
      return [];
    }

    return [
      {
        label: 'My plugin',
        items: [
          {
            label: labels[ctx.ui.locale],
            icon: 'cogs',
            pointsTo: {
              pageId: 'settings',
            },
          },
        ],
      },
    ];
  },
});
```

In this example, it can be seen that it is possible to show (or not) menu items depending on the logged-in user's permissions, or to show labels translated into the user's preferred interface language.

### Step 2: Rendering the page

Once you enter the page through one of the links, you can render the content of the custom pages by implementing the [`renderPage`](/docs/plugin-sdk/custom-pages.md#renderPage) hook:

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, RenderPageCtx } from 'datocms-plugin-sdk';

function render(component: React.ReactNode) {
  ReactDOM.render(
    <React.StrictMode>{component}</React.StrictMode>,
    document.getElementById('root'),
  );
}

connect({
  renderPage(pageId, ctx: RenderPageCtx) {
    switch (pageId) {
      case 'welcome':
        return render(<WelcomePage ctx={ctx} />);
      case 'settings':
        return render(<SettingsPage ctx={ctx} />);
      case 'analytics':
        return render(<AnalyticsPage ctx={ctx} />);
    }
  },
});
```

The strategy to adopt here is is to implement a switch that, depending on the `pageId`, will render a different, specialized React component.

The hook, in its second `ctx` argument, provides a series of information and methods for interacting with the main application. It is a good idea to pass it to the page component, in the form of a React prop.

Here's an example page component. It is important to wrap the content inside the `Canvas` component to give our app the look and feel of the DatoCMS web app:

```tsx
import { RenderPageCtx } from 'datocms-plugin-sdk';
import { Canvas } from 'datocms-react-ui';

type PropTypes = {
  ctx: RenderPageCtx,
};

function WelcomePage({ ctx }: PropTypes) {
  return (
    <Canvas ctx={ctx}>
      Hi there!
    </Canvas>
  );
}
```

#### `mainNavigationTabs(ctx)`

Use this function to declare new tabs you want to add in the top-bar of the UI.

##### Return value

The function must return: `MainNavigationTab[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderPage(pageId: string, ctx)`

This function will be called when the plugin needs to render a specific page (see the `mainNavigationTabs`, `settingsAreaSidebarItemGroups` and `contentAreaSidebarItems` functions).

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.pageId: string</summary>

The ID of the page that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderPage.ts#L19)

</details>

<details>
<summary>ctx.location</summary>

Current page location.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderPage.ts#L22)

</details>

</details>

#### `settingsAreaSidebarItemGroups(ctx)`

Use this function to declare new navigation sections in the Settings Area sidebar.

##### Return value

The function must return: `SettingsAreaSidebarItemGroup[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

---

# Plugin SDK — Sidebars and sidebar panels

Source [docs]: https://www.datocms.com/docs/plugin-sdk/sidebar-panels.md

Through plugins it is possible to customize the standard sidebars that DatoCMS offers when editing a record or an asset in the Media Area.

#### Sidebars vs Sidebar Panels

The SDK offers two ways to customize the sidebar interface. You can either add new collapsible panels to the default sidebar:

(Image content)

Or offer complete alternative sidebars, as in the example below:

(Image content)

Depending on the size of the content you need to display, you can choose one or the other. Or even offer both. You can take a look at a real-world example of both sidebars and sidebar panels in the [Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) plugin.

Inside sidebars and sidebar panels, the plugin developer can render what they prefer, while also having the possibility to:

-   access a series of information relating to the record that's being edited, the project in which the plugin is installed or the logged-in user;
-   make calls to DatoCMS to produce various effects and interact with the main application (changing form values, navigating to other pages, triggering notifications, opening modals, etc.);
    

### Implementing a Sidebar Panel

Let's say we want to create a sidebar panel that will show a link pointing to the website page related to the record we're editing.

The first step is to implement the [`itemFormSidebarPanels`](/docs/plugin-sdk/sidebar-panels.md#itemFormSidebarPanels) hook, to declare our intent to add the panel to the sidebar:

```typescript
import { connect, ItemFormSidebarPanelsCtx } from 'datocms-plugin-sdk';

connect({
  itemFormSidebarPanels(model: ItemType, ctx: ItemFormSidebarPanelsCtx) {
    return [
      {
        id: 'firstPanel',
        label: 'First panel',
        startOpen: true,
        placement: ['before', 'info'], // Where to place it relative to our default panels
        rank: 1 // Tiebreaker if two panels have the same `placement` value. Must be >= 1. Lower values are visually higher up.
      },
      {
        id: 'secondPanel',
        label: 'Second Panel',
        startOpen: true,
        placement: ['before', 'info'],
        rank: 2
      },
    ];
  },
});
```

The code above will add a panel to every record in our project... but maybe not every record in DatoCMS has a specific page in the final website, right?

It might be better to [add some settings to our plugin](/docs/plugin-sdk/config-screen.md) to let the final user declare the set of models that have permalinks, and the relative URL structure enforced on the frontend:

```typescript
itemFormSidebarPanels(model: ItemType, ctx: ItemFormSidebarPanelsCtx) {
  const { permalinksByModel } = ctx.plugin.attributes.parameters;

  // Assuming we're saving user preferences in this format:
  // {
  //   'blog_post': '/blog/:slug',
  //   'author': '/author/:slug',
  //   ...
  // }
  }

  if (!permalinksByModel[model.attributes.api_key]) {
    // Don't add the panel!
    return [];
  }

  // Add the panel!
}
```

#### Rendering the panel

The final step is to actually render the panel itself by implementing the [`renderItemFormSidebarPanel`](/docs/plugin-sdk/sidebar-panels.md#renderItemFormSidebarPanel) hook.

Inside of this hook we initialize React and render a custom component called `GoToWebsiteItemFormSidebarPanel`, passing down as a prop the second `ctx` argument, which provides a series of information and methods for interacting with the main application:

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, RenderItemFormSidebarPanelCtx } from 'datocms-plugin-sdk';

connect({
  renderItemFormSidebarPanel(
    sidebarPanelId,
    ctx: RenderItemFormSidebarPanelCtx,
  ) {
    ReactDOM.render(
      <React.StrictMode>
        <GoToWebsiteItemFormSidebarPanel ctx={ctx} />
      </React.StrictMode>,
      document.getElementById('root'),
    );
  },
});
```

A plugin might render different panels, so we can use the `sidebarPanelId` argument to know which one we are requested to render, and write a specific React component for each of them.

```tsx
import { Canvas } from 'datocms-react-ui';

type PropTypes = {
  ctx: RenderItemFormSidebarPanelCtx;
};

function GoToWebsiteItemFormSidebarPanel({ ctx }: PropTypes) {
  return (
    <Canvas ctx={ctx}>
      Hello from the sidebar!
    </Canvas>
  );
}
```

> [!WARNING] Always use the canvas!
> It is important to wrap the content inside the `Canvas` component, so that the iframe will continuously auto-adjust its size based on the content we're rendering, and to give our app the look and feel of the DatoCMS web app.

All we need to do now is to actually render the link to the website, reading from `ctx.formValues` the slug value and generating the final frontend URL:

```tsx
import { ButtonLink } from 'datocms-react-ui';

function GoToWebsiteItemFormSidebarPanel({ ctx }: PropTypes) {
  if (ctx.itemStatus === 'new') {
    // we're in a record that still has not been persisted
    return <div>Please save the record first!</div>;
  }

  const { permalinksByModel } = ctx.plugin.attributes.parameters;
  const permalinkStructure = permalinksByModel[ctx.itemType.attributes.api_key];
  const url = permalinkStructure.replace(':slug', ctx.formValues.slug);

  return (
    <Canvas ctx={ctx}>
      <ButtonLink href={url} fullWidth>
        View it on the website!
      </ButtonLink>
    </Canvas>
  );
}
```

#### Controlling Sidebar Panel positioning

To control the positioning of one or more sidebar panels, you can use the `placement` and `rank` parameters of the `itemFormSidebarPanels()` hook:

-   **The** `**placement**` **parameter** controls where your panel appears relative to the default DatoCMS panels. It accepts a two-element array like `['before', 'info']` or `['after', 'links']`:
    
    -   This is an optional parameter. If not defined, your custom panel will appear **below** all the default DatoCMS panels. This is the default behavior, and is equivalent to `placement: ['after', 'history']`.
        
    -   To place at your panel at the **top** of the sidebar, above anything else, specify `placement: ['before', 'info']`.
        
    -   If defined, the first array element must be `'before'` or `'after'`. The second array element must be the ID of one of the default existing panels: `'info'`, `'publishedVersion'`, `'schedule'`, `'links'`, or `'history'`. These correspond to the default sidebar panels of any DatoCMS record.
        
-   **The** `**rank**` **parameter** is a tie-breaker that controls what happens when multiple custom panels have the same `placement`:
    
    -   This is also is an optional parameter. When not defined, an implicit `rank: 9999` is assumed (which would normally place the panel towards the bottom).
        
    -   If defined, it must be an integer, and lower numbers are higher up visually. `0` and negative integers are OK, and will be even higher up than `rank: 1`.
        
    -   Panels with an explicit rank `>= 10000` will appear *below* panels without any explicit rank (because unranked panels are assumed to have a rank of `9999`).
        
    -   In case of a tie, panels declared earlier in the `itemFormSidebarPanels()` return array will be higher up visually.
        

**Example:**

```typescript
import { connect, ItemFormSidebarPanelsCtx } from 'datocms-plugin-sdk';

connect({
  itemFormSidebarPanels(model: ItemType, ctx: ItemFormSidebarPanelsCtx) {
    return [
      {
        id: 'firstPanel',
        label: 'First panel',
        startOpen: true,
        placement: ['before', 'info'], // Where to place it relative to our default panels
        rank: 1 // Tiebreaker if two panels have the same `placement` value. Lower values are visually higher up.
      },
      {
        id: 'secondPanel',
        label: 'Second Panel',
        startOpen: false,
        placement: ['before', 'info'],
        rank: 2
      },

      // The following two panels have no explicit rank, so they'll be auto-positioned.
      {
        id: 'otherPanel1',
        label: 'otherPanel1',
        startOpen: false,
        placement: ['before', 'info'],
      },
      {
        id: 'otherPanel2',
        label: 'otherPanel2',
        startOpen: false,
        placement: ['before', 'info'],
      },

      // This will show up after `secondPanel` but before the rankless `otherPanels`
      {
        id: 'rankConflict',
        label: 'Another panel with Rank 2',
        startOpen: false,
        placement: ['before', 'info'],
        rank: 2 // Rank conflict with another panel; we'll auto-position it
      },
    ];
  },
});
```

Becomes this sidebar:

(Image content)

Sidebar placement & rank example

### Implementing a custom Sidebar

Suppose that instead of presenting a link to a webpage, we want to embed the actual web page alongside the record. To do that we need more space than what a sidebar panel can offer, so creating a completely separate sidebar is more appropriate.

Managing sidebars is very similar to what we just did with sidebar panels. The main difference is in the way you define them. To declare our intent to add the sidebar, implement the [`itemFormSidebars`](/docs/plugin-sdk/sidebar-panels.md#itemFormSidebars) hook:

```typescript
import { connect, ItemFormSidebarsCtx } from 'datocms-plugin-sdk';

connect({
  itemFormSidebars(model: ItemType, ctx: ItemFormSidebarsCtx) {
    return [
      {
        id: "sideBySidePreview",
        label: "Side-by-side preview",
        preferredWidth: 900,
      },
    ];
  },
});
```

With the `preferredWidth`, you can control the ideal width for the sidebar when it opens. Users will then be able to resize it if they want. There is one constraint though: the sidebar width cannot exceed 60% of the screen, taking up too much screen real estate. If the `preferredWidth` is bigger than this value, it will be capped.

#### Rendering the sidebar

Now, to render the sidebar itself, we can implement the [`renderItemFormSidebar`](/docs/plugin-sdk/sidebar-panels.md#renderItemFormSidebar) hook.

Just like we did with the sidebar panel, we initialize React and render a custom component, passing down as a prop the second `ctx` argument, which provides a series of information and methods for interacting with the main application:

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, RenderItemFormSidebarCtx } from 'datocms-plugin-sdk';

connect({
  renderItemFormSidebar(
    sidebarId,
    ctx: RenderItemFormSidebarCtx,
  ) {
    ReactDOM.render(
      <React.StrictMode>
        <SideBySidePreviewSidebar ctx={ctx} />
      </React.StrictMode>,
      document.getElementById('root'),
    );
  },
});
```

A plugin might render different sidebars, so we can use the `sidebarId` argument to know which one we are requested to render, and write a specific React component for each of them.

In our `<SideBySidePreviewSidebar>` component, we can simply render an iframe pointing to the webpage, copying most of the logic from our previous sidebar panel:

```tsx
import { Canvas } from 'datocms-react-ui';

type PropTypes = {
  ctx: RenderItemFormSidebarCtx;
};

function SideBySidePreviewSidebar({ ctx }: PropTypes) {
  const { permalinksByModel } = ctx.plugin.attributes.parameters;
  const permalinkStructure = permalinksByModel[ctx.itemType.attributes.api_key];
  const url = permalinkStructure.replace(':slug', ctx.formValues.slug);

  return (
    <Canvas ctx={ctx}>
      <iframe src={url} />
    </Canvas>
  );
}
```

## Asset Sidebars and Sidebar Panels

> [!WARNING] Requires Plugin SDK v2+
> The asset sidebars and sidebar panels in the following examples requires v2.x.x or higher of the [DatoCMS Plugins SDK](https://github.com/datocms/plugins-sdk). If you're still on v1, please upgrade before proceeding.

In addition to being able to customize the sidebars of a record, it is also possible to do the same in the detail view of an asset in the Media Area:

(Image content)

(Image content)

The implementation is absolutely similar to the one just seen. The only thing that changes is the hooks to be used:

-   For sidebars: [`upload​Sidebars`](/docs/plugin-sdk/sidebar-panels.md#upload%E2%80%8BSidebars) and [`render​Upload​Sidebar`](/docs/plugin-sdk/sidebar-panels.md#render%E2%80%8BUpload%E2%80%8BSidebar)
-   For sidebar panels: [`upload​Sidebar​Panels`](/docs/plugin-sdk/sidebar-panels.md#upload%E2%80%8BSidebar%E2%80%8BPanels) and [`render​Upload​SidebarPanel`](/docs/plugin-sdk/sidebar-panels.md#render%E2%80%8BUpload%E2%80%8BSidebarPanel)
    

Here's an example:

```typescript
import {
  connect,
  UploadSidebarPanelsCtx,
  RenderUploadSidebarPanelCtx,
  UploadSidebarsCtx,
  RenderUploadSidebarCtx
} from 'datocms-plugin-sdk';

connect({
  uploadSidebars(ctx: UploadSidebarsCtx) {
    return [
      {
        id: "customSidebar",
        label: "My Custom Sidebar",
        preferredWidth: 900,
      },
    ];
  },
  renderUploadSidebar(sidebarId: string, ctx: RenderUploadSidebarCtx) {
    render(<CustomSidebar ctx={ctx} />);
  },

  uploadSidebarPanels(ctx: UploadSidebarPanelsCtx) {
    return [
      {
        id: 'customSidebarPanel',
        label: 'Custom Sidebar Panel',
        startOpen: true,
      },
    ];
  },
  renderUploadSidebarPanel(sidebarPanelId: string, ctx: RenderUploadSidebarPanelCtx) {
    render(<CustomSidebarPanel ctx={ctx} />);
  },
});
```

#### `itemFormSidebars(itemType: ItemType, ctx)`

Use this function to declare new sidebar to be shown when the user edits records of a particular model.

##### Return value

The function must return: `ItemFormSidebar[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderItemFormSidebar(sidebarId: string, ctx)`

This function will be called when the plugin needs to render a sidebar (see the `itemFormSidebars` hook).

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>Item form additional methods</summary>

These methods can be used to interact with the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.toggleField(path: string, show: boolean) => Promise<void></summary>

Hides/shows a specific field in the form. Please be aware that when a field is hidden, the field editor for that field will be removed from the DOM itself, including any associated plugins. When it is shown again, its plugins will be reinitialized.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L68)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.toggleField(fieldPath, true);
```

</details>
<details>
<summary>ctx.disableField(path: string, disable: boolean) => Promise<void></summary>

Disables/re-enables a specific field in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L83)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.disableField(fieldPath, true);
```

</details>
<details>
<summary>ctx.scrollToField(path: string, locale?: string) => Promise<void></summary>

Smoothly navigates to a specific field in the form. If the field is localized it will switch language tab and then navigate to the chosen field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L100)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.scrollToField(fieldPath);
```

</details>
<details>
<summary>ctx.setFieldValue(path: string, value: unknown) => Promise<void></summary>

Changes a specific path of the `formValues` object.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L115)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.setFieldValue(fieldPath, 'new value');
```

</details>
<details>
<summary>ctx.formValuesToItem(...)</summary>

Takes the internal form state, and transforms it into an Item entity compatible with DatoCMS API.

When `skipUnchangedFields`, only the fields that changed value will be serialized.

If the required nested blocks are still not loaded, this method will return `undefined`.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L132)

```ts
await ctx.formValuesToItem(ctx.formValues, false);
```

</details>
<details>
<summary>ctx.itemToFormValues(...)</summary>

Takes an Item entity, and converts it into the internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L145)

```ts
await ctx.itemToFormValues(ctx.item);
```

</details>
<details>
<summary>ctx.saveCurrentItem(showToast?: boolean) => Promise<void></summary>

Triggers a submit form for current record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L157)

```ts
await ctx.saveCurrentItem();
```

</details>

</details>

<details>
<summary>Item form additional properties</summary>

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

<details>
<summary>Properties and methods</summary>

<details>
<summary>ctx.sidebarId: string</summary>

The ID of the sidebar that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderItemFormSidebar.ts#L25)

</details>

<details>
<summary>ctx.parameters: Record<string, unknown></summary>

The arbitrary `parameters` of the declared in the `itemFormSidebars` function.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderItemFormSidebar.ts#L30)

</details>

</details>

</details>

#### `itemFormSidebarPanels(itemType: ItemType, ctx)`

Use this function to declare new sidebar panels to be shown when the user edits records of a particular model.

##### Return value

The function must return: `ItemFormSidebarPanel[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderItemFormSidebarPanel(sidebarPaneId: string, ctx)`

This function will be called when the plugin needs to render a sidebar panel (see the `itemFormSidebarPanels` hook).

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>Item form additional methods</summary>

These methods can be used to interact with the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.toggleField(path: string, show: boolean) => Promise<void></summary>

Hides/shows a specific field in the form. Please be aware that when a field is hidden, the field editor for that field will be removed from the DOM itself, including any associated plugins. When it is shown again, its plugins will be reinitialized.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L68)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.toggleField(fieldPath, true);
```

</details>
<details>
<summary>ctx.disableField(path: string, disable: boolean) => Promise<void></summary>

Disables/re-enables a specific field in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L83)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.disableField(fieldPath, true);
```

</details>
<details>
<summary>ctx.scrollToField(path: string, locale?: string) => Promise<void></summary>

Smoothly navigates to a specific field in the form. If the field is localized it will switch language tab and then navigate to the chosen field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L100)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.scrollToField(fieldPath);
```

</details>
<details>
<summary>ctx.setFieldValue(path: string, value: unknown) => Promise<void></summary>

Changes a specific path of the `formValues` object.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L115)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.setFieldValue(fieldPath, 'new value');
```

</details>
<details>
<summary>ctx.formValuesToItem(...)</summary>

Takes the internal form state, and transforms it into an Item entity compatible with DatoCMS API.

When `skipUnchangedFields`, only the fields that changed value will be serialized.

If the required nested blocks are still not loaded, this method will return `undefined`.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L132)

```ts
await ctx.formValuesToItem(ctx.formValues, false);
```

</details>
<details>
<summary>ctx.itemToFormValues(...)</summary>

Takes an Item entity, and converts it into the internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L145)

```ts
await ctx.itemToFormValues(ctx.item);
```

</details>
<details>
<summary>ctx.saveCurrentItem(showToast?: boolean) => Promise<void></summary>

Triggers a submit form for current record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L157)

```ts
await ctx.saveCurrentItem();
```

</details>

</details>

<details>
<summary>Item form additional properties</summary>

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

<details>
<summary>Properties and methods</summary>

<details>
<summary>ctx.sidebarPaneId: string</summary>

The ID of the sidebar panel that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderItemFormSidebarPanel.ts#L25)

</details>

<details>
<summary>ctx.parameters: Record<string, unknown></summary>

The arbitrary `parameters` of the panel declared in the `itemFormSidebarPanels` function.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderItemFormSidebarPanel.ts#L31)

</details>

</details>

</details>

#### `uploadSidebars(ctx)`

Use this function to declare new sidebar to be shown when the user opens up an asset in the Media Area.

##### Return value

The function must return: `UploadSidebar[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderUploadSidebar(sidebarId: string, ctx)`

This function will be called when the plugin needs to render a sidebar (see the `uploadSidebars` hook).

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.sidebarId: string</summary>

The ID of the sidebar that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderUploadSidebar.ts#L21)

</details>

<details>
<summary>ctx.parameters: Record<string, unknown></summary>

The arbitrary `parameters` of the declared in the `uploadSidebars` function.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderUploadSidebar.ts#L27)

</details>

<details>
<summary>ctx.upload: Upload</summary>

The active asset.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderUploadSidebar.ts#L30)

</details>

</details>

#### `uploadSidebarPanels(ctx)`

Use this function to declare new sidebar panels to be shown when the user opens up an asset in the Media Area.

##### Return value

The function must return: `UploadSidebarPanel[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderUploadSidebarPanel(sidebarPaneId: string, ctx)`

This function will be called when the plugin needs to render a sidebar panel (see the `uploadSidebarPanels` hook).

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.sidebarPaneId: string</summary>

The ID of the sidebar panel that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderUploadSidebarPanel.ts#L24)

</details>

<details>
<summary>ctx.parameters: Record<string, unknown></summary>

The arbitrary `parameters` of the panel declared in the `uploadSidebarPanels` function.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderUploadSidebarPanel.ts#L30)

</details>

<details>
<summary>ctx.upload: Upload</summary>

The active asset.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderUploadSidebarPanel.ts#L33)

</details>

</details>

---

# Plugin SDK — Outlets

Source [docs]: https://www.datocms.com/docs/plugin-sdk/form-outlets.md

Through plugins, it's possible to customize various areas of the DatoCMS interface. We call these customizable areas "outlets".

Outlets are essentially iframes where plugin developers can render custom content, providing enhanced functionality and user experiences within the DatoCMS ecosystem.

Outlets offer the ability to:

-   Access information related to records, projects, or logged-in users
-   Make calls to DatoCMS to produce various effects and interact with the main application (e.g., changing values, navigating, triggering notifications, opening modals)
    
-   Customize the user interface to fit specific workflow needs
    

If you prefer, a form outlet can also be completely hidden from the interface (setting his height to zero), and work under the cover to tweak the default behaviour of DatoCMS.

##### Types of Outlets

DatoCMS allows you to configure outlets in various areas of the interface.

# Record Form Outlets

Record form outlets allow you to add custom areas above the record editing form:

(Image content)

#### Implementing a Record Form Outlet

The first step is to implement the [`itemFormOutlets`](/docs/plugin-sdk/form-outlets.md#itemFormOutlets) hook, to declare our intent to add the outlet to the form:

```typescript
import { connect, ItemFormOutletsCtx } from 'datocms-plugin-sdk';

connect({
  itemFormOutlets(model, ctx: ItemFormOutletsCtx) {
    return [
      {
        id: 'myOutlet',
        initialHeight: 100,
      },
    ];
  },
});
```

The `initialHeight` property sets the initial height of the frame, while the plugin itself is loading. It can also be useful to completely hide the outlet, by passing the value zero to it.

The code above will add the outlet to the form of every record in our project, but you can also [add some settings to the plugin](/docs/plugin-sdk/config-screen.md) to ie. let the final user pick only some specific models:

```typescript
itemFormOutlets(model, ctx: ItemFormOutletsCtx) {
  const { modelApiKeys } = ctx.plugin.attributes.parameters;

  if (!modelApiKeys.includes(model.attributes.api_key)) {
    // Don't add the outlet!
    return [];
  }

  // Add the outlet!
}
```

The final step is to actually render the outlet itself by implementing the [`renderItemFormOutlet`](/docs/plugin-sdk/form-outlets.md#renderItemFormOutlet) hook.

Inside of this hook we can initialize React and render a custom component, passing down as a prop the second `ctx` argument, which provides a series of information and methods for interacting with the main application:

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, RenderItemFormOutletCtx, ItemFormOutletsCtx } from 'datocms-plugin-sdk';

connect({
  itemFormOutlets(model, ctx: ItemFormOutletsCtx) { ... },
  renderItemFormOutlet(
    outletId,
    ctx: RenderItemFormOutletCtx,
  ) {
    ReactDOM.render(
      <React.StrictMode>
        <MyCustomOutlet ctx={ctx} />
      </React.StrictMode>,
      document.getElementById('root'),
    );
  },
});
```

A plugin might render different types of form outlets, so we can use the `outletId` argument to know which one we are requested to render, and write a specific React component for each of them.

```tsx
import { Canvas } from 'datocms-react-ui';

function MyCustomOutlet({ ctx }) {
  return (
    <Canvas ctx={ctx}>
      Hello from the record form outlet!
    </Canvas>
  );
}
```

> [!WARNING] Always use the canvas!
> If you want to render something inside the outlet, it is important to wrap the content inside the `Canvas` component, so that the iframe will continuously auto-adjust its size based on the content we're rendering, and to give our app the look and feel of the DatoCMS web app.
> 
> If you want the outlet to be hidden from the interface, just return `null` and set an `initialHeight: 0` in the `itemFormOutlets` hook.

# Record Collection Outlets

Record collection outlets allow you to add custom areas to the page that displays a collection of records for a specific model.

(Image content)

(Image content)

The implementation is exactly the same as the one we just saw for the Record Form Outlets. The only thing that changes is the hooks to be used:

-   To declare the intention to offer Record Collection Outlets, use [`itemCollectionOutlets`](/docs/plugin-sdk/form-outlets.md#itemCollectionOutlets);
-   To actually render the outlets, use [`renderItemCollectionOutlet`](/docs/plugin-sdk/form-outlets.md#renderItemCollectionOutlet).
    

Here's a full example:

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, ItemCollectionOutletsCtx, RenderItemCollectionOutletCtx } from 'datocms-plugin-sdk';
import { Canvas, Button } from 'datocms-react-ui';

connect({
  itemCollectionOutlets(model, ctx: ItemCollectionOutletsCtx) {
    // Optional: Add conditions to show the outlet only for specific models
    const { modelApiKeys } = ctx.plugin.attributes.parameters;
    if (!modelApiKeys.includes(model.attributes.api_key)) {
      return [];
    }

    return [
      {
        id: 'myCollectionOutlet',
        initialHeight: 100,
      },
    ];
  },
  renderItemCollectionOutlet(outletId, ctx: RenderItemCollectionOutletCtx) {
    render(<MyCustomCollectionOutlet ctx={ctx} />);
  },
});

function MyCustomCollectionOutlet({ ctx }) {
  return (
    <Canvas ctx={ctx}>
      <h3>Custom Collection Outlet</h3>
      <p>This outlet appears above the record listing for {ctx.itemType.attributes.name}.</p>
    </Canvas>
  );
}
```

#### `itemCollectionOutlets(itemType: ItemType, ctx)`

Use this function to declare custom outlets to be shown at the top of a collection of records of a particular model.

##### Return value

The function must return: `ItemCollectionOutlet[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderItemCollectionOutlet(itemCollectionOutletId: string, ctx)`

This function will be called when the plugin needs to render an outlet defined by the `itemCollectionOutlets()` hook.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.itemCollectionOutletId: string</summary>

The ID of the outlet that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderItemCollectionOutlet.ts#L24)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for which the outlet is being rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderItemCollectionOutlet.ts#L26)

</details>

</details>

#### `itemFormOutlets(itemType: ItemType, ctx)`

Use this function to declare custom outlets to be shown at the top of the record's editing page.

##### Return value

The function must return: `ItemFormOutlet[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderItemFormOutlet(itemFormOutletId: string, ctx)`

This function will be called when the plugin needs to render an outlet defined by the `itemFormOutlets()` hook.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>Item form additional methods</summary>

These methods can be used to interact with the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.toggleField(path: string, show: boolean) => Promise<void></summary>

Hides/shows a specific field in the form. Please be aware that when a field is hidden, the field editor for that field will be removed from the DOM itself, including any associated plugins. When it is shown again, its plugins will be reinitialized.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L68)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.toggleField(fieldPath, true);
```

</details>
<details>
<summary>ctx.disableField(path: string, disable: boolean) => Promise<void></summary>

Disables/re-enables a specific field in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L83)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.disableField(fieldPath, true);
```

</details>
<details>
<summary>ctx.scrollToField(path: string, locale?: string) => Promise<void></summary>

Smoothly navigates to a specific field in the form. If the field is localized it will switch language tab and then navigate to the chosen field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L100)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.scrollToField(fieldPath);
```

</details>
<details>
<summary>ctx.setFieldValue(path: string, value: unknown) => Promise<void></summary>

Changes a specific path of the `formValues` object.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L115)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.setFieldValue(fieldPath, 'new value');
```

</details>
<details>
<summary>ctx.formValuesToItem(...)</summary>

Takes the internal form state, and transforms it into an Item entity compatible with DatoCMS API.

When `skipUnchangedFields`, only the fields that changed value will be serialized.

If the required nested blocks are still not loaded, this method will return `undefined`.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L132)

```ts
await ctx.formValuesToItem(ctx.formValues, false);
```

</details>
<details>
<summary>ctx.itemToFormValues(...)</summary>

Takes an Item entity, and converts it into the internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L145)

```ts
await ctx.itemToFormValues(ctx.item);
```

</details>
<details>
<summary>ctx.saveCurrentItem(showToast?: boolean) => Promise<void></summary>

Triggers a submit form for current record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L157)

```ts
await ctx.saveCurrentItem();
```

</details>

</details>

<details>
<summary>Item form additional properties</summary>

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

<details>
<summary>Properties and methods</summary>

<details>
<summary>ctx.itemFormOutletId: string</summary>

The ID of the outlet that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderItemFormOutlet.ts#L25)

</details>

</details>

</details>

---

# Plugin SDK — Field extensions

Source [docs]: https://www.datocms.com/docs/plugin-sdk/field-extensions.md

By creating what we call 'field extensions', plugins can change the way in which the fields of a record are presented to the final editor, going beyond the appearance configurations that DatoCMS offers by default.

There are different types of field extensions that can be created, depending on requirements:

#### "Field editor" extensions

They operate on top of a particular field, replacing the default field editor that DatoCMS provides with custom code:

(Image content)

The use cases are varied, and many examples are already on our marketplace, ready to be installed on your project:

-   The Shopify product plugin can be hooked into string fields and completely changes the interface to allow you to browse the products in your Shopify store, then save the ID of the selected product in the string field itself;
-   The Hidden field plugin simply hides a specific field from the editor's eyes, while the Conditional fields plugin shows/hides a number of fields when you toggle a particular checkbox field.
    

###### Field editors as sidebar panels

It is also possible to move editor extensions to the right-hand sidebar, giving it the appearance of a collapsible panel. The difference between this mode and a [sidebar panel](/docs/plugin-sdk/sidebar-panels.md) is that this controls a specific field of the record and can use it as a "storage unit" to save internal information, while a sidebar panel is not associated with any particular field.

As an example, the Sidebar notes plugin uses this mode to turn a JSON field into a kind of notepad where you can add virtual post-it notes.

#### "Field addon" field extensions

As the name suggests, addons do not change the way a field is edited, but they add functionality, or provide additional information, directly below the field editor. While only one editor can be set up for each field, it is possible to have several addons per field, each providing its own different functionality:

(Image content)

As examples of use, Yandex Translate adds a button below your localisable text/string fields to automatically translate its content from one locale to another, while Sanitize HTML allows you to clean up the HTML code present in a text field according to various preferences.

> [!POSITIVE] Two sides of the same coin
> Editors and addons are both field extensions, so they have access to exactly the same methods and information. The difference between the two is simply semantics: editors are for editing the field, while addons offer extra functionality.

### How to hook field extensions to a field

The SDK provides an [`overrideFieldExtensions`](/docs/plugin-sdk/field-extensions.md#overrideFieldExtensions) hook that can be implemented to declare the intention to take part in the rendering of any field within the form, either by setting its editor, or by adding some addons, or both.

In this example, we are forcing the use of a custom `starRating` editor for all integer fields that have an ID of `rating`:

```typescript
import { connect, Field, FieldIntentCtx } from 'datocms-plugin-sdk';

connect({
  overrideFieldExtensions(field: Field, ctx: FieldIntentCtx) {
    if (
      field.attributes.field_type === 'integer' &&
      field.attributes.api_key === 'rating'
    ) {
      return {
        editor: { id: 'starRating' },
      };
    }
  },
});
```

Similarly, we can also add an addon extension called `loremIpsumGenerator` below all the text fields:

```typescript
overrideFieldExtensions(field: Field, ctx: FieldIntentCtx) {
  if (field.attributes.field_type === 'text') {
    return {
      addons: [
        { id: 'loremIpsumGenerator' },
      ],
    };
  }
}
```

### Rendering the field extension

At this point, we need to actually render the field extensions by implementing the [`renderFieldExtension`](/docs/plugin-sdk/field-extensions.md#renderFieldExtension) hook.

Inside of this hook we can implement a simple "router" that will present a different React component depending on the field extension that we've requested to render inside the `iframe`.

We also make sure to pass down as a prop the second `ctx` argument, which provides a series of information and methods for interacting with the main application:

```tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, RenderFieldExtensionCtx } from 'datocms-plugin-sdk';

function render(component: React.ReactNode) {
  ReactDOM.render(
    <React.StrictMode>{component}</React.StrictMode>,
    document.getElementById('root'),
  );
}

connect({
  renderFieldExtension(fieldExtensionId: string, ctx: RenderFieldExtensionCtx) {
    switch (fieldExtensionId) {
      case 'starRating':
        return render(<StarRatingEditor ctx={ctx} />);
      case 'loremIpsumGenerator':
        return render(<LoremIpsumGenerator ctx={ctx} />);
    }
  },
});
```

The implementation of the Lorem Ipsum component is pretty straightforward: we simply use the `ctx.setFieldValue` function to change the value of the field into a randomly generated string:

```tsx
import { Canvas, Button } from 'datocms-react-ui';
import { loremIpsum } from 'lorem-ipsum';

type PropTypes = {
  ctx: RenderFieldExtensionCtx;
};

function LoremIpsumGenerator({ ctx }: PropTypes) {
  const insertLoremIpsum = () => {
    ctx.setFieldValue(ctx.fieldPath, loremIpsum({ format: 'plain' }));
  };

  return (
    <Canvas ctx={ctx}>
      <Button type="button" onClick={insertLoremIpsum} buttonSize="xxs">
        Add lorem ipsum
      </Button>
    </Canvas>
  );
}
```

It is important to wrap the content inside the `Canvas` component, so that the iframe will continuously auto-adjust its size based on the content we're rendering, and to give our app the look and feel of the DatoCMS web app.

The Star Rating component is quite similar. We get the current field value from `ctx.formValues` and the disabled state from `ctx.disabled`. When the user interacts with the component and changes its value, we call `ctx.setFieldValue` to propagate the change to the main DatoCMS application:

```tsx
import ReactStars from 'react-rating-stars-component';
import get from 'lodash/get';
import { Canvas } from 'datocms-react-ui';
import { RenderFieldExtensionCtx } from 'datocms-plugin-sdk';

type PropTypes = {
  ctx: RenderFieldExtensionCtx;
};

function StarRatingEditor({ ctx }: PropTypes) {
  const currentValue = get(ctx.formValues, ctx.fieldPath);
  const handleChange = (newValue: number) => {
    ctx.setFieldValue(ctx.fieldPath, newValue);
  };
  return (
    <Canvas ctx={ctx}>
      <ReactStars
        size={32}
        isHalf={false}
        edit={!ctx.disabled}
        value={currentValue || 0}
        onChange={handleChange}
      />
    </Canvas>
  );
}
```

Here's the final result:

(Video content)

### Adding user-defined settings into the mix

You might have noticed that our plugin is currently hardcoding some choices, namely:

-   the rules that decide when to apply both our "star rating" and "lorem ipsum" extensions;
-   the maximum number of stars to show;
    
-   the length of the "lorem ipsum" text we're generating;
    

If we want, we could make these settings configurable by the user, either by implementing some [global plugin settings](/docs/plugin-sdk/config-screen.md), or by transforming our field extensions into ["manual" extensions](https://www.datocms.com/docs/plugin-sdk/manual-field-extensions.md "/docs/plugin-sdk/sdk/manual-field-extensions").

When to use one strategy or the other is completely up to you, and each has its own advantages/disadvanges.

-   Manual field extensions are, well, manually hooked by the end-user on each field, and for each installation different configuration options can be specified. Given that our star rating extension will most likely be used in a few specific places rather than in all integer fields of the project, manual fields might be the best choice.
-   On the other hand, our Lorem Ipsum generator may be convenient in all text fields, so requiring the end user to manually install it everywhere would be unnecessarily tedious. In this case, the choice to force the addon on all fields with the [`overrideFieldExtensions`](/docs/plugin-sdk/field-extensions.md#overrideFieldExtensions) hook is probably the right one.
    

In the [next section](/docs/plugin-sdk/manual-field-extensions.md) we're going to take a much more detailed look at manual field extensions, and we're going to convert our star rating editor into a manual extension.

> [!NOTE] User-defined settings are updated in real-time
> When user-defined settings are saved, they are persisted and propagated in real-time to other users.

### Reference Table: Field Types & Internal Names

This table lists the internal names of different DatoCMS field types. It is useful for limiting your field extensions only to specific field types. If you're using TypeScript, you can also get this from the type `FieldAttributes['field_type']` [exported from our CMA client](https://github.com/datocms/js-rest-api-clients/blob/v3.4.1/packages/cma-client/src/generated/SimpleSchemaTypes.ts#L6280-L6308).

For more details on the different DatoCMS field types, please see the [CMA documentation on Fields](/docs/content-management-api/resources/field.md#available-field-types).

| Field Type | Internal Name (for `attributes.field_type`) |
| --- | --- |
| Single-line string | `string` |
| Multi-line text | `text` |
| Boolean | `boolean` |
| Integer | `integer` |
| Float | `float` |
| Date | `date` |
| Date & Time | `date_time` |
| Color | `color` |
| JSON | `json` |
| Location | `lat_lon` |
| SEO and Social | `seo` |
| Slug | `slug` |
| External Video | `video` |
| Single Asset | `file` |
| Asset Gallery | `gallery` |
| Single Link (to another record) | `link` |
| Multiple Links (to other records) | `links` |
| Modular Content | `rich_text` |
| Single Block | `single_block` |
| Structured Text | `structured_text` |

### Side note: `ctx` updates and React useEffect

**This section is only relevant if your plugin has** `**useEffects**` **triggered by context changes.**

Because plugins live inside an iframe, record updates may sometimes cause the `ctx` (context) object to be recreated and passed through the iframe again, triggering a React `useEffect` unexpectedly even if the values appear the same. This is because `useEffect` compares objects by reference, not value equality. A re-created `ctx` object with the same values will still cause React to believe it's changed.

For example, if you update some field values in the CMS (outside your plugin), `ctx.formValues` will update as expected, because those values are different. However, React will also think `ctx.fields` has changed, even though its values remain the same.

Generally this shouldn't be a problem, but if you specifically need to make sure a `useEffect` only runs on actual value changes, we recommend a [custom hook like useDeepCompareEffect()](https://github.com/kentcdodds/use-deep-compare-effect).

#### `overrideFieldExtensions(field: Field, ctx)`

Use this function to automatically force one or more field extensions to a particular field.

##### Return value

The function must return: `FieldExtensionOverride | undefined`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.itemType: ItemType</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/overrideFieldExtensions.ts#L31)

</details>

</details>

#### `renderFieldExtension(fieldExtensionId: string, ctx)`

This function will be called when the plugin needs to render a field extension (see the `manualFieldExtensions` and `overrideFieldExtensions` functions).

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>Field additional properties</summary>

These information describe the current state of the field where this plugin is applied to.

<details>
<summary>ctx.disabled: boolean</summary>

Whether the field is currently disabled or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L12)

</details>

<details>
<summary>ctx.fieldPath: string</summary>

The path in the `formValues` object where to find the current value for the field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L17)

</details>

<details>
<summary>ctx.field: Field</summary>

The field where the field extension is installed to.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L19)

</details>

<details>
<summary>ctx.parentField: Field | undefined</summary>

If the field extension is installed in a field of a block, returns the top level Modular Content/Structured Text field containing the block itself.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L24)

</details>

<details>
<summary>ctx.block</summary>

If the field extension is installed in a field of a block, returns the ID of the block — or `undefined` if the block is still not persisted — and the block model.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L30)

</details>

</details>

<details>
<summary>Item form additional methods</summary>

These methods can be used to interact with the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.toggleField(path: string, show: boolean) => Promise<void></summary>

Hides/shows a specific field in the form. Please be aware that when a field is hidden, the field editor for that field will be removed from the DOM itself, including any associated plugins. When it is shown again, its plugins will be reinitialized.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L68)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.toggleField(fieldPath, true);
```

</details>
<details>
<summary>ctx.disableField(path: string, disable: boolean) => Promise<void></summary>

Disables/re-enables a specific field in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L83)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.disableField(fieldPath, true);
```

</details>
<details>
<summary>ctx.scrollToField(path: string, locale?: string) => Promise<void></summary>

Smoothly navigates to a specific field in the form. If the field is localized it will switch language tab and then navigate to the chosen field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L100)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.scrollToField(fieldPath);
```

</details>
<details>
<summary>ctx.setFieldValue(path: string, value: unknown) => Promise<void></summary>

Changes a specific path of the `formValues` object.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L115)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.setFieldValue(fieldPath, 'new value');
```

</details>
<details>
<summary>ctx.formValuesToItem(...)</summary>

Takes the internal form state, and transforms it into an Item entity compatible with DatoCMS API.

When `skipUnchangedFields`, only the fields that changed value will be serialized.

If the required nested blocks are still not loaded, this method will return `undefined`.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L132)

```ts
await ctx.formValuesToItem(ctx.formValues, false);
```

</details>
<details>
<summary>ctx.itemToFormValues(...)</summary>

Takes an Item entity, and converts it into the internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L145)

```ts
await ctx.itemToFormValues(ctx.item);
```

</details>
<details>
<summary>ctx.saveCurrentItem(showToast?: boolean) => Promise<void></summary>

Triggers a submit form for current record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L157)

```ts
await ctx.saveCurrentItem();
```

</details>

</details>

<details>
<summary>Item form additional properties</summary>

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

<details>
<summary>Properties and methods</summary>

<details>
<summary>ctx.fieldExtensionId: string</summary>

The ID of the field extension that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderFieldExtension.ts#L29)

</details>

<details>
<summary>ctx.parameters: Record<string, unknown></summary>

The arbitrary `parameters` of the field extension.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderFieldExtension.ts#L31)

</details>

</details>

</details>

---

# Plugin SDK — Manual field extensions

Source [docs]: https://www.datocms.com/docs/plugin-sdk/manual-field-extensions.md

> [!WARNING]
> In the [previous chapter](/docs/plugin-sdk/field-extensions.md):
> 
> -   we saw the different types of field extensions we can create ([editors](/docs/plugin-sdk/field-extensions.md#field-editor-extensions) and [addons](/docs/plugin-sdk/field-extensions.md#field-addon-field-extensions));
>     
> -   we've seen how we can programmatically associate a particular extension to one (or multiple) fields;
>     
> -   we used the [`renderFieldExtension`](/docs/plugin-sdk/field-extensions.md#rendering-the-field-extension) hook to actually render our extensions.
>     
> 
> If you haven't read the chapter, we encourage you to do it, as we're going to build up on the same examples!

### Manual field extensions vs `overrideFieldExtensions`

So far, we have used the [`overrideFieldExtensions`](/docs/plugin-sdk/field-extensions.md#how-to-hook-field-extensions-to-a-field) hook to programmatically apply our extensions to fields. There is an alternative way of working with field extensions that passes through a second hook that you can implement, namely [`manualFieldExtensions`](/docs/plugin-sdk/manual-field-extensions.md#manualFieldExtensions):

```typescript
import { connect, Field, ManualFieldExtensionsCtx, OverrideFieldExtensionsCtx } from 'datocms-plugin-sdk';

connect({
  manualFieldExtensions(ctx: ManualFieldExtensionsCtx) {
    return [
      {
        id: 'starRating',
        name: 'Star rating',
        type: 'editor',
        fieldTypes: ['integer'],
      },
    ];
  },
  overrideFieldExtensions(field: Field, ctx: OverrideFieldExtensionsCtx) {
    if (field.attributes.field_type === 'text') {
      return {
        addons: [{ id: 'loremIpsumGenerator' }],
      };
    }
  },
});
```

With this setup, we are still automatically applying our "Lorem ipsum" generator to every text field in our project, but the "Star rating" is becoming a manual extension. That is, **it's the end-user that will have to manually apply** it on one or more fields of type "integer" through the "Presentation" tab in the field settings:

(Video content)

### When to use one strategy or the other?

At this point a question may arise... when does it make sense to force an extension with `overrideFieldExtensions` and when to let the user install it manually? Well, it all depends on the type of extension you're developing, and what you imagine to be the most comfortable and natural way to offer its functionality!

Let's try to think about the extensions we have developed so far, and see what would be the best strategy for them:

-   Given that the "Star rating" extension will most likely be used in a few specific spots, rather than in all integer fields of the project, letting the user manually apply it when needed feels like the best choice.
-   On the other hand, our "Lorem Ipsum generator" is probably convenient in all text fields: requiring the end user to manually install it everywhere could be unnecessarily tedious, so the choice to programmatically force the addon on all text fields is probably the right one.
    

If we feel that a carpet-bombing strategy for the "Lorem ipsum" extension might bee too much, and we wanted to make the installation more granular but still automatic, we could add some [global settings](/docs/plugin-sdk/config-screen.md) to the plugin to allow the user to configure some application rules (ie. "only add the addon if the API key of the text field ends with `_main_content`"):

```typescript
overrideFieldExtensions(field: Field, ctx: OverrideFieldExtensionsCtx) {
  // get the suffix from plugin configuration settings
  const { loremIpsumApiKeySuffix } = ctx.plugin.attributes.parameters;

  if (
    field.attributes.field_type === 'text' &&
    field.attributes.api_key.endsWith(loremIpsumApiKeySuffix)
  ) {
    return {
      addons: [
        { id: 'loremIpsumGenerator' },
      ],
    };
  }
}
```

If you can't make up your mind on the best strategy for your field extension, there's always a third option: let the end user be in charge of the decision! Plugin settings are always available in every hook, so you can read the user preference and act accordingly:

```typescript
import { connect, Field, ManualFieldExtensionsCtx, OverrideFieldExtensionsCtx } from 'datocms-plugin-sdk';

connect({
  manualFieldExtensions(ctx: ManualFieldExtensionsCtx) {
    const { autoApply } = ctx.plugin.attributes.parameters;

    if (autoApply) {
      return [];
    }

    return [
      {
        id: 'starRating',
        name: 'Star rating',
        type: 'editor',
        fieldTypes: ['integer'],
      },
      {
        id: 'loremIpsumGenerator',
        name: 'Lorem Ipsum generator',
        type: 'addon',
        fieldTypes: ['text'],
      },
    ];
  },
  overrideFieldExtensions(field: Field, ctx: OverrideFieldExtensionsCtx) {
    const { autoApply } = ctx.plugin.attributes.parameters;

    if (!autoApply) {
      return;
    }

    if (field.attributes.field_type === 'text') {
      return {
        addons: [{ id: 'loremIpsumGenerator' }],
      };
    }

    if (
      field.attributes.field_type === 'integer' &&
      field.attributes.api_key === 'rating'
    ) {
      return {
        editor: { id: 'starRating' },
      };
    }
  },
});
```

### Add per-field config screens to manual field extensions

(Image content)

In the `manualFieldExtensions()` hook, we can pass the `configurable: true` option to declare that we want to present a config screen to the user when they're installing the extension on a field:

```typescript
import { connect, Field, ManualFieldExtensionsCtx } from 'datocms-plugin-sdk';

connect({
  manualFieldExtensions(ctx: ManualFieldExtensionsCtx) {
    return [
      {
        id: 'starRating',
        name: 'Star rating',
        type: 'editor',
        fieldTypes: ['integer'],
        configurable: true,
      },
    ];
  },
});
```

To continue our example, let's take our "Star rating" editor and say we want to offer end-users the ability, on a per-field basis, to specify the maximum number of stars that can be selected and the color of the stars.

Just like global plugin settings, these per-field configuration parameters are **completely arbitrary**, so it is up to the plugin itself to show the user a form through which they can be changed.

> [!WARNING] Don't use form management libraries!
> Unlike the global config screen, where we manage the form ourselves, here **we are "guests" inside the field edit form**. That is, the submit button in the modal triggers the saving not only of our settings, but also of all the other field configurations, which we do not control.
> 
> The SDK, in this location, provides a set of very simple primitives to integrate with the form managed by the DatoCMS application, including validations. The use of React form management libraries is not suitable in this hook, as most of them are designed to "control" the form.

The hook provided to render the config screen is [`renderManualFieldExtensionConfigScreen`](/docs/plugin-sdk/manual-field-extensions.md#renderManualFieldExtensionConfigScreen), and it will be called by DatoCMS when the user adds the extension on a particular field.

Inside the hook we simply initialize React and a custom component called `StarRatingConfigScreen`. The argument `ctx` provides a series of information and methods for interacting with the main application, and for now all we just pass the whole object to the component, in the form of a React prop:

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import {
  connect,
  RenderManualFieldExtensionConfigScreenCtx,
} from 'datocms-plugin-sdk';

connect({
  renderManualFieldExtensionConfigScreen(
    fieldExtensionId: string,
    ctx: RenderManualFieldExtensionConfigScreenCtx,
  ) {
    ReactDOM.render(
      <React.StrictMode>
        <StarRatingConfigScreen ctx={ctx} />
      </React.StrictMode>,
      document.getElementById('root'),
    );
  },
});
```

This is how our full component looks like:

```typescript
import { RenderManualFieldExtensionConfigScreenCtx } from 'datocms-plugin-sdk';
import { Canvas, Form, TextField } from 'datocms-react-ui';
import { CSSProperties, useCallback, useState } from 'react';

type PropTypes = {
  ctx: RenderManualFieldExtensionConfigScreenCtx;
};

// this is how we want to save our settings
type Parameters = {
  maxRating: number;
  starsColor: NonNullable<CSSProperties['color']>;
};

function StarRatingConfigScreen({ ctx }: PropTypes) {
  const [formValues, setFormValues] = useState<Partial<Parameters>>(
    ctx.parameters,
  );

  const update = useCallback((field, value) => {
    const newParameters = { ...formValues, [field]: value };
    setFormValues(newParameters);
    ctx.setParameters(newParameters);
  }, [formValues, setFormValues, ctx.setParameters]);

  return (
    <Canvas ctx={ctx}>
      <Form>
        <TextField
          id="maxRating"
          name="maxRating"
          label="Maximum rating"
          required
          value={formValues.maxRating}
          onChange={update.bind(null, 'maxRating')}
        />
        <TextField
          id="starsColor"
          name="starsColor"
          label="Stars color"
          required
          value={formValues.starsColor}
          onChange={update.bind(null, 'starsColor')}
        />
      </Form>
    </Canvas>
  );
}
```

Here's how it works:

-   we use `ctx.parameters` as the initial value for our internal state `formValues`;
-   as the user changes values for the inputs, we're use `ctx.setParameters()` to propagate the change to the main DatoCMS application (as well as updating our internal state).
    

> [!WARNING] Always use the canvas!
> It is important to wrap the content inside the `Canvas` component, so that the iframe will continuously auto-adjust its size based on the content we're rendering, and to give our app the look and feel of the DatoCMS web app.

### Enforcing validations on configuration options

Users might insert invalid values for the options we present. We can implement another hook called [`validateManualFieldExtensionParameters`](/docs/plugin-sdk/manual-field-extensions.md#validateManualFieldExtensionParameters) to enforce some validations on them:

```typescript
const isValidCSSColor = (strColor: string) => {
  const s = new Option().style;
  s.color = strColor;
  return s.color !== '';
};

connect({
  validateManualFieldExtensionParameters(
    fieldExtensionId: string,
    parameters: Record<string, any>,
  ) {
    const errors: Record<string, string> = {};

    if (
      isNaN(parseInt(parameters.maxRating)) ||
      parameters.maxRating < 2 ||
      parameters.maxRating > 10
    ) {
      errors.maxRating = 'Rating must be between 2 and 10!';
    }

    if (!parameters.starsColor || !isValidCSSColor(parameters.starsColor)) {
      errors.starsColor = 'Invalid CSS color!';
    }

    return errors;
  },
});
```

Inside our component, we can access those errors and present them below the input fields:

```typescript
function StarRatingParametersForm({ ctx }: PropTypes) {
  const errors = ctx.errors as Partial<Record<string, string>>;

  // ...

  return (
    <Canvas ctx={ctx}>
      <TextField
          id="maxRating"
          /* ... */
          error={errors.maxRating}
        />
        <TextField
          id="starsColor"
          /* ... */
          error={errors.starsColor}
        />
    </Canvas>
  );
}
```

This is the final result:

(Video content)

Now that we have some settings, we can access them in the `renderFieldExtension` hook through the `ctx.parameters` object, and use them to configure the star rating component:

```typescript
import ReactStars from 'react-rating-stars-component';

function StarRatingEditor({ ctx }: PropTypes) {
  // ...

  return (
    <ReactStars
      /* ... */
      count={ctx.parameters.maxRating}
      activeColor={ctx.parameters.starsColor}
    />
  );
}
```

#### `manualFieldExtensions(ctx)`

Use this function to declare new field extensions that users will be able to install manually in some field.

##### Return value

The function must return: `ManualFieldExtension[]`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `validateManualFieldExtensionParameters(fieldExtensionId: string, parameters: Record<string, unknown>)`

This function will be called each time the configuration object changes. It must return an object containing possible validation errors.

##### Return value

The function must return: `Record<string, unknown> | Promise<Record<string, unknown>>`.

---

# Plugin SDK — Dropdown actions

Source [docs]: https://www.datocms.com/docs/plugin-sdk/dropdown-actions.md

Plugins can greatly improve DatoCMS's overall functionality by defining custom actions that will appear as dropdown menu items or context menus throughout the entire interface, enhancing a more personalized user experience.

Custom dropdown actions can be defined in various unique contexts:

## Record-Editing actions

Actions of this specific type will be prominently displayed next to the title of the record, enabling users to quickly access tools custom to streamline their editing process:

(Image content)

The hooks required for their implementation are:

-   Present the actions using [`itemFormDropdownActions()`](/docs/plugin-sdk/dropdown-actions.md#itemFormDropdownActions)
-   Execute the action with [`executeItemFormDropdownAction()`](/docs/plugin-sdk/dropdown-actions.md#executeItemFormDropdownAction)
    

## Field-Specific Record Actions

Actions will be conveniently placed in a dropdown next to the field label, allowing users to easily interact with the data contained in a specific field of a record:

(Image content)

The hooks required for their implementation are:

-   Present the actions using [`fieldDropdownActions()`](/docs/plugin-sdk/dropdown-actions.md#fieldDropdownActions)
-   Execute the action with [`executeFieldDropdownAction()`](/docs/plugin-sdk/dropdown-actions.md#executeFieldDropdownAction)
    

## Global Record Actions

Actions of this type will be presented in two areas of the interface: firstly, in the batch actions available within the record collection view:

(Image content)

And secondly, in the detailed view of the record itself (together with any available Record-Editing actions), providing users with additional options for manipulation:

(Image content)

The hooks required for their implementation are:

-   Present the actions using [`itemsDropdownActions()`](/docs/plugin-sdk/dropdown-actions.md#itemsDropdownActions)
-   Execute the action with [`executeItemsDropdownAction()`](/docs/plugin-sdk/dropdown-actions.md#executeItemsDropdownAction)
    

## Asset Management Actions

Actions of this type will be displayed in two sections of the interface: in batch actions within the Media Area:

(Image content)

and in the detailed view of the asset itself:

(Image content)

The hooks required for their implementation are:

-   Present the actions using [`uploadsDropdownActions()`](/docs/plugin-sdk/dropdown-actions.md#uploadsDropdownActions)
-   Execute the action with [`executeUploadsDropdownAction()`](/docs/plugin-sdk/dropdown-actions.md#executeUploadsDropdownAction)
    

## How to implement a dropdown action

This is a brief example of how you can implement your actions:

```typescript
import {
  connect,
  type FieldDropdownActionsCtx,
  type ExecuteFieldDropdownActionCtx,
} from "datocms-plugin-sdk";
import "datocms-react-ui/styles.css";

connect({
  fieldDropdownActions(field, ctx: FieldDropdownActionsCtx) {
    if (
      ctx.itemType.attributes.api_key !== "blog_post" ||
      field.attributes.api_key !== "title"
    ) {
      // Don't add any action!
      return [];
    }

    return [
      // A single action
      {
        id: "actionA",
        label: "Custom action A",
        icon: "music",
      },
      // A group of actions
      {
        label: "Group of custom actions",
        icon: "mug-hot",
        actions: [
          // These actions will be shown in a submenu
          {
            id: "actionB",
            label: "Custom action B",
            icon: "rocket-launch",
          },
          {
            id: "actionC",
            label: "Custom action C",
            icon: "sparkles",
          },
        ],
      },
    ];
  },
  async executeFieldDropdownAction(
    actionId: string,
    ctx: ExecuteFieldDropdownActionCtx,
  ) {
    if (actionId === "actionA") {
      // Do something using ctx
      ctx.notice('Selected action A');
    } else if (actionId === "actionB") {
      // Do something else
      ctx.notice('Selected action B');
    } else if (actionId === "actionC") {
      // Do something else
      ctx.notice('Selected action C');
    }
  },
});
```

The types of operations you can perform within your execute hooks are dependent on the methods available in the `ctx` argument, which in turn is influenced by the specific type of action. As an example, Record-Editing and Field-Specific Record actions offer methods in `ctx` to change the state of the record form, while other actions do not. Consult the specific documentation for each hook listed below to understand the available options.

#### `executeFieldDropdownAction(actionId: string, ctx)`

Use this function to execute a particular dropdown action defined via the `fieldDropdownActions()` hook.

##### Return value

The function must return: `Promise<void>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>Field additional properties</summary>

These information describe the current state of the field where this plugin is applied to.

<details>
<summary>ctx.disabled: boolean</summary>

Whether the field is currently disabled or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L12)

</details>

<details>
<summary>ctx.fieldPath: string</summary>

The path in the `formValues` object where to find the current value for the field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L17)

</details>

<details>
<summary>ctx.field: Field</summary>

The field where the field extension is installed to.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L19)

</details>

<details>
<summary>ctx.parentField: Field | undefined</summary>

If the field extension is installed in a field of a block, returns the top level Modular Content/Structured Text field containing the block itself.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L24)

</details>

<details>
<summary>ctx.block</summary>

If the field extension is installed in a field of a block, returns the ID of the block — or `undefined` if the block is still not persisted — and the block model.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L30)

</details>

</details>

<details>
<summary>Item form additional methods</summary>

These methods can be used to interact with the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.toggleField(path: string, show: boolean) => Promise<void></summary>

Hides/shows a specific field in the form. Please be aware that when a field is hidden, the field editor for that field will be removed from the DOM itself, including any associated plugins. When it is shown again, its plugins will be reinitialized.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L68)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.toggleField(fieldPath, true);
```

</details>
<details>
<summary>ctx.disableField(path: string, disable: boolean) => Promise<void></summary>

Disables/re-enables a specific field in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L83)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.disableField(fieldPath, true);
```

</details>
<details>
<summary>ctx.scrollToField(path: string, locale?: string) => Promise<void></summary>

Smoothly navigates to a specific field in the form. If the field is localized it will switch language tab and then navigate to the chosen field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L100)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.scrollToField(fieldPath);
```

</details>
<details>
<summary>ctx.setFieldValue(path: string, value: unknown) => Promise<void></summary>

Changes a specific path of the `formValues` object.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L115)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.setFieldValue(fieldPath, 'new value');
```

</details>
<details>
<summary>ctx.formValuesToItem(...)</summary>

Takes the internal form state, and transforms it into an Item entity compatible with DatoCMS API.

When `skipUnchangedFields`, only the fields that changed value will be serialized.

If the required nested blocks are still not loaded, this method will return `undefined`.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L132)

```ts
await ctx.formValuesToItem(ctx.formValues, false);
```

</details>
<details>
<summary>ctx.itemToFormValues(...)</summary>

Takes an Item entity, and converts it into the internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L145)

```ts
await ctx.itemToFormValues(ctx.item);
```

</details>
<details>
<summary>ctx.saveCurrentItem(showToast?: boolean) => Promise<void></summary>

Triggers a submit form for current record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L157)

```ts
await ctx.saveCurrentItem();
```

</details>

</details>

<details>
<summary>Item form additional properties</summary>

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

<details>
<summary>Properties and methods</summary>

<details>
<summary>ctx.parameters: Record<string, unknown> | undefined</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/executeFieldDropdownAction.ts#L25)

</details>

</details>

</details>

#### `executeItemFormDropdownAction(actionId: string, ctx)`

Use this function to execute a particular dropdown action defined via the `itemFormDropdownActions()` hook.

##### Return value

The function must return: `Promise<void>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>Item form additional methods</summary>

These methods can be used to interact with the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.toggleField(path: string, show: boolean) => Promise<void></summary>

Hides/shows a specific field in the form. Please be aware that when a field is hidden, the field editor for that field will be removed from the DOM itself, including any associated plugins. When it is shown again, its plugins will be reinitialized.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L68)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.toggleField(fieldPath, true);
```

</details>
<details>
<summary>ctx.disableField(path: string, disable: boolean) => Promise<void></summary>

Disables/re-enables a specific field in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L83)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.disableField(fieldPath, true);
```

</details>
<details>
<summary>ctx.scrollToField(path: string, locale?: string) => Promise<void></summary>

Smoothly navigates to a specific field in the form. If the field is localized it will switch language tab and then navigate to the chosen field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L100)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.scrollToField(fieldPath);
```

</details>
<details>
<summary>ctx.setFieldValue(path: string, value: unknown) => Promise<void></summary>

Changes a specific path of the `formValues` object.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L115)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.setFieldValue(fieldPath, 'new value');
```

</details>
<details>
<summary>ctx.formValuesToItem(...)</summary>

Takes the internal form state, and transforms it into an Item entity compatible with DatoCMS API.

When `skipUnchangedFields`, only the fields that changed value will be serialized.

If the required nested blocks are still not loaded, this method will return `undefined`.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L132)

```ts
await ctx.formValuesToItem(ctx.formValues, false);
```

</details>
<details>
<summary>ctx.itemToFormValues(...)</summary>

Takes an Item entity, and converts it into the internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L145)

```ts
await ctx.itemToFormValues(ctx.item);
```

</details>
<details>
<summary>ctx.saveCurrentItem(showToast?: boolean) => Promise<void></summary>

Triggers a submit form for current record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L157)

```ts
await ctx.saveCurrentItem();
```

</details>

</details>

<details>
<summary>Item form additional properties</summary>

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

<details>
<summary>Properties and methods</summary>

<details>
<summary>ctx.parameters: Record<string, unknown> | undefined</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/executeItemFormDropdownAction.ts#L23)

</details>

</details>

</details>

#### `executeItemsDropdownAction(actionId: string, items: Item[], ctx)`

Use this function to execute a particular dropdown action defined via the `itemsDropdownActions()` hook.

##### Return value

The function must return: `Promise<void>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.parameters: Record<string, unknown> | undefined</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/executeItemsDropdownAction.ts#L23)

</details>

</details>

#### `executeSchemaItemTypeDropdownAction(actionId: string, itemType: ItemType, ctx)`

Use this function to execute a particular dropdown action defined via the `schemaItemTypeDropdownActions()` hook.

##### Return value

The function must return: `Promise<void>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.parameters: Record<string, unknown> | undefined</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/executeSchemaItemTypeDropdownAction.ts#L23)

</details>

</details>

#### `executeUploadsDropdownAction(actionId: string, uploads: Upload[], ctx)`

Use this function to execute a particular dropdown action defined via the `uploadsDropdownActions()` hook.

##### Return value

The function must return: `Promise<void>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.parameters: Record<string, unknown> | undefined</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/executeUploadsDropdownAction.ts#L23)

</details>

</details>

#### `fieldDropdownActions(field: Field, ctx)`

Use this function to define custom actions (or groups of actions) to be displayed at the individual field level in the record editing form.

The `executeFieldDropdownAction()` hook will be triggered once the user clicks on one of the defined actions.

##### Return value

The function must return: `Array<DropdownAction | DropdownActionGroup>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>Field additional properties</summary>

These information describe the current state of the field where this plugin is applied to.

<details>
<summary>ctx.disabled: boolean</summary>

Whether the field is currently disabled or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L12)

</details>

<details>
<summary>ctx.fieldPath: string</summary>

The path in the `formValues` object where to find the current value for the field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L17)

</details>

<details>
<summary>ctx.field: Field</summary>

The field where the field extension is installed to.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L19)

</details>

<details>
<summary>ctx.parentField: Field | undefined</summary>

If the field extension is installed in a field of a block, returns the top level Modular Content/Structured Text field containing the block itself.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L24)

</details>

<details>
<summary>ctx.block</summary>

If the field extension is installed in a field of a block, returns the ID of the block — or `undefined` if the block is still not persisted — and the block model.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/field.ts#L30)

</details>

</details>

<details>
<summary>Item form additional properties</summary>

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

</details>

#### `itemFormDropdownActions(itemType: ItemType, ctx)`

Use this function to define custom actions (or groups of actions) to be displayed at when editing a particular record.

The `executeItemFormDropdownAction()` hook will be triggered once the user clicks on one of the defined actions.

##### Return value

The function must return: `Array<DropdownAction | DropdownActionGroup>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

These information describe the current state of the form that's being shown to the end-user to edit a record.

<details>
<summary>ctx.locale: string</summary>

The currently active locale for the record.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L12)

</details>

<details>
<summary>ctx.item: Item | null</summary>

If an already persisted record is being edited, returns the full record entity.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L17)

</details>

<details>
<summary>ctx.itemType: ItemType</summary>

The model for the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L19)

</details>

<details>
<summary>ctx.formValues: Record<string, unknown></summary>

The complete internal form state.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L21)

</details>

<details>
<summary>ctx.itemStatus: 'new' | 'draft' | 'updated' | 'published'</summary>

The current status of the record being edited.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L23)

</details>

<details>
<summary>ctx.isSubmitting: boolean</summary>

Whether the form is currently submitting itself or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L25)

</details>

<details>
<summary>ctx.isFormDirty: boolean</summary>

Whether the form has some non-persisted changes or not.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L27)

</details>

<details>
<summary>ctx.blocksAnalysis: BlocksAnalysis</summary>

Provides information on how many blocks are currently present in the form.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/ctx/commonExtras/itemForm.ts#L29)

</details>

</details>

#### `itemsDropdownActions(itemType: ItemType, ctx)`

This function lets you set up custom actions (or groups of actions) that show up when the user:

-   selects multiple records in the collection view for batch operations, or
-   starts editing a specific record.

The `executeItemsDropdownAction()` hook will be triggered once the user clicks on one of the defined actions.

##### Return value

The function must return: `Array<DropdownAction | DropdownActionGroup>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.itemType: ItemType</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/itemsDropdownActions.ts#L27)

</details>

</details>

#### `schemaItemTypeDropdownActions(itemType: ItemType, ctx)`

Use this function to define custom actions (or groups of actions) for a model/block model in the Schema section.

The `executeSchemaItemTypeDropdownAction()` hook will be triggered once the user clicks on one of the defined actions.

##### Return value

The function must return: `Array<DropdownAction | DropdownActionGroup>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `uploadsDropdownActions(ctx)`

This function lets you set up custom actions (or groups of actions) that show up when the user:

-   selects multiple assets in the Media Area for batch operations, or
-   opens up a specific asset from the Media Area.

The `executeUploadsDropdownAction()` hook will be triggered once the user clicks on one of the defined actions.

##### Return value

The function must return: `Array<DropdownAction | DropdownActionGroup>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

---

# Plugin SDK — Structured Text customizations

Source [docs]: https://www.datocms.com/docs/plugin-sdk/structured-text-customizations.md

[Structured Text](/docs/structured-text/dast.md) is a consciously simple format, with a very small number of possible nodes — only the ones that are really helpful to capture the semantics of a standard piece of content, and with zero possibility to introduce styling and break the decoupling of content from presentation.

This is generally a very good thing, as it makes working on the frontend extremely simple and predictable: unlike HTML or Markdown, you don't have to be defensive and worry about some complex nesting of tags that you'd never think it could be possible, or unwanted styles coming from the editors.

There are, however, situations where it is critical to be able to **add a small, controlled set of styles to your content, to represent nuances of different semantics** within a piece of content.

### Adding custom styles to nodes

Let's take this article as an example:

(Image content)

The third paragraph is conceptually similar to the others, but it's obviously more important, and we want the reader to pay more attention to what it says.

In these cases, we can use plugins to specify alternative styles for paragraph and heading nodes using the `customBlockStylesForStructuredTextField` hook:

```tsx
import { connect, Field, FieldIntentCtx } from 'datocms-plugin-sdk';

connect({
  customBlockStylesForStructuredTextField(field: Field, ctx: FieldIntentCtx) {
    return [
      {
        id: 'emphasized',
        node: 'paragraph',
        label: 'Emphasized',
        appliedStyle: {
          fontFamily: 'Georgia',
          fontStyle: 'italic',
          fontSize: '1.4em',
          lineHeight: '1.2',
        }
      }
    ];
  },
});
```

The code above will add a custom `"emphasized"` style for `paragraph` nodes to every Structured Text field in the project. The `appliedStyle` property lets you customize how the style will be rendered inside of DatoCMS, when the user selects it:

(Video content)

You can also use the first argument of the hook (`field`) to only allow custom styles in some specific Structured Text fields. If that's the case, you'll probably want to [add some settings to the plugin](/docs/plugin-sdk/config-screen.md) to let the final user decide which they are:

```tsx
customBlockStylesForStructuredTextField(field: Field, ctx: FieldIntentCtx) {
  const { fieldsInWhichAllowCustomStyles } = ctx.plugin.attributes.parameters;

  if (!fieldsInWhichAllowCustomStyles.includes[field.attributes.api_key)) {
    // No custom styles!
    return [];
  }

  return [
    {
      id: 'emphasized',
      node: 'paragraph',
      // ...
    },
  ];
}
```

The final Structured Text value will have the custom style applied in the `style` property:

```json
{
  "type": "root",
  "children": [
    {
      "type": "paragraph",
      "style": "emphasized",
      "children": [
        {
          "type": "span",
          "value": "Hello!"
        }
      ]
    }
  ]
}
```

### Adding custom marks

The default Structured Text editor already supports a number of [different marks](/docs/structured-text/dast.md#span) (`strong`, `code`, `underline`, `highlight`, etc.), but you might want to annotate parts of the text using custom marks.

An example would be adding a "spoiler" mark, to signal a portion of text that we don't want to show the visitor unless they explicitly accept a spoiler alert.

The `customMarksForStructuredTextField` hook lets you do exactly that:

```tsx
import { connect, Field, FieldIntentCtx } from 'datocms-plugin-sdk';

connect({
  customMarksForStructuredTextField(field: Field, ctx: FieldIntentCtx) {
    return [
      {
        id: 'spoiler',
        label: 'Spoiler',
        icon: 'bomb',
        keyboardShortcut: 'mod+shift+l',
        appliedStyle: {
          backgroundColor: 'rgba(255, 0, 0, 0.3)',
        },
      },
    ];
  },
});
```

The code above will add a custom `"spoiler"` mark to every Structured Text field in the project. The `appliedStyle` property lets you customize how the style will be rendered inside of DatoCMS, when the user selects it:

(Video content)

The final result on the Structured Text value will be the following:

```json
{
  "type": "root",
  "children": [
    {
      "type": "paragraph",
      "children": [
        {
          "type": "span",
          "value": "In the "
        },
        {
          "type": "span",
          "marks": ["spoiler"],
          "value": "final killing scene"
        },
        {
          "type": "span",
          "value": ", the director really outdid himself."
        }
      ]
    }
  ]
}
```

> [!WARNING] You're in charge of the frontend!
> All of our Structured Text management libraries (React, Vue, etc.) allow you to specify custom rendering rules. When working with custom styles and marks, it's up to your frontend to decide how to render them!

#### `customBlockStylesForStructuredTextField(field: Field, ctx)`

Use this function to define a number of custom block styles for a specific Structured Text field.

##### Return value

The function must return: `StructuredTextCustomBlockStyle[] | undefined`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.itemType: ItemType</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/customBlockStylesForStructuredTextField.ts#L29)

</details>

</details>

#### `customMarksForStructuredTextField(field: Field, ctx)`

Use this function to define a number of custom marks for a specific Structured Text field.

##### Return value

The function must return: `StructuredTextCustomMark[] | undefined`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.itemType: ItemType</summary>

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/customMarksForStructuredTextField.ts#L30)

</details>

</details>

---

# Plugin SDK — Asset sources

Source [docs]: https://www.datocms.com/docs/plugin-sdk/asset-sources.md

By default, to add new assets to the Media Area through the interface, you can upload files from your computer. But plugins can define custom asset sources to allow contributors to upload assets from external providers.

For example, the Unsplash plugin in our Marketplace allows to upload royalty-free high-resolution images:

(Video content)

## Define custom asset sources

Within a plugin you can define the [`assetSources`](/docs/plugin-sdk/asset-sources.md#assetSources) hook to expose new asset sources. Every source must specify an internal ID, and a name and a representative icon that will be shown in the interface.

```typescript
import { connect } from 'datocms-plugin-sdk';

connect({
  assetSources() {
    return [
      {
        id: 'unsplash',
        name: 'Unsplash',
        icon: {
          type: 'svg',
          viewBox: '0 0 448 512',
          content:
            '<path fill="currentColor" d="M448,230.17V480H0V230.17H141.13V355.09H306.87V230.17ZM306.87,32H141.13V156.91H306.87Z" class=""></path>',
        },
        modal: {
          width: 'm',
        },
      },
    ];
  },
});
```

## Rendering the custom asset source

When the user selects the custom source, a modal will be opened with the size you specified, and the [`renderAssetSource`](/docs/plugin-sdk/asset-sources.md#renderAssetSource) hook will be called. Inside of this hook we initialize React and render a custom component called `AssetBrowser`, passing down as a prop the second `ctx` argument, which provides a series of information and methods for interacting with the main application:

```typescript
import { connect } from 'datocms-plugin-sdk';

connect({
  assetSources() {
    return [{...}];
  },
  renderAssetSource(sourceId: string, ctx: RenderAssetSourceCtx) {
    render(<AssetBrowser ctx={ctx} />);
  },
});
```

As we just saw, a plugin might offer different asset sources, so we can use the `sourceId` argument to know which one we are requested to render, and write a specific React component for each of them.

```typescript
import { Canvas, RenderAssetSourceCtx } from 'datocms-react-ui';

type PropTypes = {
  ctx: RenderAssetSourceCtx;
};

function AssetBrowser({ ctx }: PropTypes) {
  return (
    <Canvas ctx={ctx}>
      Hello from the sidebar!
    </Canvas>
  );
}
```

> [!WARNING] Always use the canvas!
> It is important to wrap the content inside the `Canvas` component, so that the iframe will continuously auto-adjust its size based on the content we're rendering, and to give our app the look and feel of the DatoCMS web app.

We can use this component to render whatever we want. The important thing is to call the `ctx.select` method to communicate to the main DatoCMS app the selected asset URL:

```typescript
import { ButtonLink } from 'datocms-react-ui';

function AssetBrowser({ ctx }: PropTypes) {
  const handleSelect = () => {
    ctx.select({
      resource: {
        url: 'https://unsplash.com/photos/yf8qPXQFDJE',
        filename: `sky.jpg`,
      },
    });
  }

  return (
    <Canvas ctx={ctx}>
      <Button onClick={handleSelect}>Select</Button>
    </Canvas>
  );
}
```

If you're generating your asset on the fly (ie. by rendering on a canvas), instead of a regular URL you can also pass a base64-encoded data URI:

```typescript
ctx.select({
  resource: {
    base64: 'data:image/png;base64,PD94bWwgd..',
    filename: `generated-image.png`,
  },
});
```

You can also optionally specify some metadata to associate with the newly created upload:

```typescript
ctx.select({
  resource: {
    url:
      'https://images.unsplash.com/photo-1416339306562-f3d12fefd36f',
    filename: 'man-drinking-coffee.jpg',
  },
  copyright: 'Royalty free (Unsplash)',
  author: 'Jeff Sheldon',
  notes: 'A man drinking a coffee',
  tags: ['man', 'coffee'],
});
```

#### `assetSources(ctx)`

Use this function to declare additional sources to be shown when users want to upload new assets.

##### Return value

The function must return: `AssetSource[] | undefined`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `renderAssetSource(assetSourceId: string, ctx)`

This function will be called when the user selects one of the plugin's asset sources to upload a new media file.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.assetSourceId: string</summary>

The ID of the assetSource that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderAssetSource.ts#L18)

</details>

<details>
<summary>ctx.select(newUpload: NewUpload) => void</summary>

Function to be called when the user selects the asset: it will trigger the creation of a new `Upload` that will be added in the Media Area.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderAssetSource.ts#L40)

```ts
await ctx.select({
  resource: {
    url: 'https://images.unsplash.com/photo-1416339306562-f3d12fefd36f',
    filename: 'man-drinking-coffee.jpg',
  },
  copyright: 'Royalty free (Unsplash)',
  author: 'Jeff Sheldon',
  notes: 'A man drinking a coffee',
  tags: ['man', 'coffee'],
});
```

</details>

</details>

---

# Plugin SDK — Opening modals

Source [docs]: https://www.datocms.com/docs/plugin-sdk/modals.md

Within all the `renderXXX` hooks — that is, those that have the task of presenting a custom interface part to the user — it is possible to open custom modal dialogs to "get out" of the reduced space that the `iframe` provides, and get more room to build more complex interfaces.

Suppose our plugin implements a [custom page](/docs/plugin-sdk/custom-pages.md) accessible from the top navigation bar:

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
import { connect, MainNavigationTabsCtx, RenderPageCtx } from 'datocms-plugin-sdk';
import { Canvas } from 'datocms-react-ui';

function render(component: React.ReactNode) {
  ReactDOM.render(
    <React.StrictMode>{component}</React.StrictMode>,
    document.getElementById('root'),
  );
}

connect({
  mainNavigationTabs(ctx: MainNavigationTabsCtx) {
    return [
      {
        label: 'Welcome',
        icon: 'igloo',
        pointsTo: {
          pageId: 'welcome',
        },
      },
    ];
  },
  renderPage(pageId, ctx: RenderPageCtx) {
    switch (pageId) {
      case 'welcome':
        return render(<WelcomePage ctx={ctx} />);
    }
  },
});

type PropTypes = {
  ctx: RenderPageCtx;
};

function WelcomePage({ ctx }: PropTypes) {
  return <Canvas ctx={ctx}>Hi!</Canvas>;
}
```

Within the `ctx` argument you can find the function `openModal()`, which triggers the opening of a modal:

```typescript
import { Canvas, Button } from 'datocms-react-ui';

function WelcomePage({ ctx }: PropTypes) {
  const handleOpenModal = async () => {
    const result = await ctx.openModal({
      id: 'customModal',
      title: 'Custom title!',
      width: 'l',
      parameters: { name: 'Mark' },
    });
    ctx.notice(result);
  };

  return (
    <Canvas ctx={ctx}>
      <Button type="button" onClick={handleOpenModal}>
        Open modal!
      </Button>
    </Canvas>
  );
}
```

The `openModal()` function offers various rendering options, for example you can set its size and title. Interestingly, the function returns a promise, which will be resolved when the modal is closed by the user.

You can specify what to render inside the modal by implementing a new hook called [`renderModal`](/docs/plugin-sdk/modals.md#renderModal) which, similarly to what we did with custom pages, initializes React with a custom component:

```typescript
connect({
  renderModal(modalId: string, ctx: RenderModalCtx) {
    switch (modalId) {
      case 'customModal':
        return render(<CustomModal ctx={ctx} />);
    }
  },
});
```

You are free to fill the modal with the information you want, and you can access the parameters specified when opening the modal through `ctx.parameters`:

```typescript
import { Canvas } from 'datocms-react-ui';

type PropTypes = {
  ctx: RenderModalCtx;
};

function CustomModal({ ctx }: PropTypes) {
  return (
    <Canvas ctx={ctx}>
      <div style={{ fontSize: 'var(--font-size-xxxl)', fontWeight: '500' }}>
        Hello {ctx.parameters.name}!
      </div>
    </Canvas>
  );
}
```

As with any other hook, it is important to wrap the content inside the `Canvas` component, so that the iframe will continuously auto-adjust its size based on the content we're rendering, and to give our app the look and feel of the DatoCMS web app.

### Closing the modal

If the modal will be closed through the close button provided by the interface, the promise `openModal()` will be resolved with value `null`.

You can also decide not to show a "close" button:

```typescript
const result = await sdk.openModal({
  id: 'customModal',
  // ...
  closeDisabled: true,
});
```

In this case the user will only be able to close the modal via an interaction of your choice (custom buttons, for example):

```typescript
import { Canvas, Button } from 'datocms-react-ui';

function CustomModal({ ctx }: PropTypes) {
  const handleClose = (returnValue: string) => {
    ctx.resolve(returnValue);
  };

  return (
    <Canvas ctx={ctx}>
      Hello {ctx.parameters.name}!
      <Button type="button" onClick={handleClose.bind(null, 'a')}>Close A</Button>
      <Button type="button" onClick={handleClose.bind(null, 'b')}>Close B</Button>
    </Canvas>;
}
```

The `ctx.resolve()` function will close the modal, and resolve the original `openModal()` promise with the value you passed.

#### `renderModal(modalId: string, ctx)`

This function will be called when the plugin requested to open a modal (see the `openModal` hook).

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.modalId: string</summary>

The ID of the modal that needs to be rendered.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderModal.ts#L17)

</details>

<details>
<summary>ctx.parameters: Record<string, unknown></summary>

The arbitrary `parameters` of the modal declared in the `openModal` function.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderModal.ts#L22)

</details>

<details>
<summary>ctx.resolve(returnValue: unknown) => Promise<void></summary>

A function to be called by the plugin to close the modal. The `openModal` call will be resolved with the passed return value.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/renderModal.ts#L40)

```ts
const returnValue = prompt(
  'Please specify the value to return to the caller:',
  'success',
);

await ctx.resolve(returnValue);
```

</details>

</details>

---

# Plugin SDK — Event hooks

Source [docs]: https://www.datocms.com/docs/plugin-sdk/event-hooks.md

In addition to all the `render<LOCATION>` hooks, the SDK also exposes a number of hooks that can be useful to intercept specific events happening on the interface, and execute custom code, or change the way the regular interface behaves.

All these event hooks follow the same `on<EVENT>` naming convention.

### Execute custom code when the plugin loads up

There are situations where a plugin needs to execute code as soon as the DatoCMS interface is loaded. For example, a plugin may need to contact third party systems to verify some information, or maybe notify the user in some way.

In these scenarios you can use the [`onBoot`](/docs/plugin-sdk/event-hooks.md#onBoot) hook, and have the guarantee that it will be called as soon as the main DatoCMS application is loaded:

```tsx
import { connect } from 'datocms-plugin-sdk';

connect({
  async onBoot(ctx) {
    ctx.notice('Hi there!');
  }
});
```

Inside this hook there is no point in rendering anything, because it won't be displayed anywhere. For a concrete use case of this hook, please have a look at the chapter [Releasing new plugin versions](/docs/plugin-sdk/releasing-new-plugin-versions.md).

### Intercept actions on records

Another useful group of event hooks can be used to intercept when the user wants to perform a specific action on one (or multiple) records:

-   [`onBeforeItemUpsert`](/docs/plugin-sdk/event-hooks.md#onBeforeItemUpsert): called when the user wants to save a record (both creation or update);
-   [`onBeforeItemsDestroy`](/docs/plugin-sdk/event-hooks.md#onBeforeItemsDestroy): called when the user wants to delete one (or more) records;
    
-   [`onBeforeItemsPublish`](/docs/plugin-sdk/event-hooks.md#onBeforeItemsPublish): called when the user wants to publish one (or more) records;
-   [`onBeforeItemsUnpublish`](/docs/plugin-sdk/event-hooks.md#onBeforeItemsUnpublish): called when the user wants to unpublish one (or more) records;
    

All these hooks can return the value `false` to stop the relative action from happening.

In the following example we're using the `onBeforeItemUpsert` hook to check if the user is saving articles with the "highlighted" flag turned on, and if that's the case we show them an additional confirmation, to make sure they know what they're doing:

```tsx
import { connect } from 'datocms-plugin-sdk';

connect({
  async onBeforeItemUpsert(createOrUpdateItemPayload, ctx) {
    const item = createOrUpdateItemPayload.data;

    // get the ID of the Article model
    const articleItemTypeId = Object.values(ctx.itemTypes).find(itemType => itemType.attributes.api_key === 'article').id;

    // fast return for any record that's not an Article
    if (item.relationships.item_type.data.id !== articleItemTypeId) {
      return;
    }

    // fast return if the article is not highlighted
    if (!item.attributes.highlighted) {
      return;
    }

    const confirmation = await ctx.openConfirm({
      title: 'Mark Article as highlighted?',
      content: 'Highlighted articles are displayed on the homepage of the site!',
      cancel: { label: 'Cancel', value: false },
      choices: [
        { label: 'Yes, save as highlighted', value: true, intent: 'negative' },
      ],
    });

    if (!confirmation) {
      ctx.notice('The article has not been saved, you can unflag the "highlighted" field.');
      // returning false blocks the action
      return false;
    }
  }
});
```

We can also do something similar to confirm if the user really wants to publish a record. The `onBeforeItemsPublish` hook is also called when the user is selecting multiple records from the collection page, and applying a batch publish operation:

```tsx
import { connect } from 'datocms-plugin-sdk';

connect({
  async onBeforeItemsPublish(items, ctx) {
    return await ctx.openConfirm({
      title: `Publish ${items.length} records?`,
      content: `This action will make the records visibile on the public website!`,
      cancel: { label: 'Cancel', value: false },
      choices: [{ label: 'Yes, publish', value: true }],
    });
  }
});
```

#### `onBoot(ctx)`

This function will be called once at boot time and can be used to perform ie. some initial integrity checks on the configuration.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `onBeforeItemsDestroy(items: Item[], ctx)`

This function will be called before destroying records. You can stop the action by returning `false`.

##### Return value

The function must return: `MaybePromise<boolean>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `onBeforeItemsPublish(items: Item[], ctx)`

This function will be called before publishing records. You can stop the action by returning `false`.

##### Return value

The function must return: `MaybePromise<boolean>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `onBeforeItemsUnpublish(items: Item[], ctx)`

This function will be called before unpublishing records. You can stop the action by returning `false`.

##### Return value

The function must return: `MaybePromise<boolean>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

#### `onBeforeItemUpsert(createOrUpdateItemPayload: ItemUpdateSchema | ItemCreateSchema, ctx)`

This hook is called when the user attempts to save a record. You can use it to block record saving.

If you return `false`, the record will NOT be saved. A small on-page error will say "A plugin blocked the action". However, for better UX, consider also using `ctx.alert()` to better explain to the user why their save was blocked.

If you return `true`, the save will proceed as normal.

This hook runs BEFORE serverside validation. You can use it to do your own additional validation before returning. Clientside validations are not affected by this hook, since those occur on individual fields' `onBlur()` events.

##### Return value

The function must return: `MaybePromise<boolean>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

<details>
<summary>Hook-specific properties and methods</summary>

This hook exposes additional information and operations specific to the context in which it operates.

<details>
<summary>ctx.scrollToField(path: string, locale?: string) => Promise<void></summary>

Smoothly navigates to a specific field in the form. If the field is localized it will switch language tab and then navigate to the chosen field.

[View on Github](https://github.com/datocms/plugins-sdk/blob/master/packages/sdk/src/hooks/onBeforeItemUpsert.ts#L47)

```ts
const fieldPath = prompt(
  'Please insert the path of a field in the form',
  ctx.fieldPath,
);

await ctx.scrollToField(fieldPath);
```

</details>

</details>

---

# Plugin SDK — Customize record presentation

Source [docs]: https://www.datocms.com/docs/plugin-sdk/customize-presentation.md

When viewing a collection of items, the records will normally show their title and possibly an image preview (as defined in the model's presentation settings). In this example, the record previews come from the record's `Name` field:

(Image content)

But sometimes you may want more advanced control over the presentation of your collections. For example, you might want to make the title dynamically change based on another field in the record or an external API query.

### Basic Example: Data from another field

Maybe you want to show an emoji next to the product name based on its product type:

(Image content)

This change is purely cosmetic & superficial, affecting only what your editors see in the collection. It does NOT change the actual data in the record, only its *presentation* inside the DatoCMS UI.

How does it work? We used the [`buildItemPresentationInfo`](/docs/plugin-sdk/customize-presentation.md#buildItemPresentationInfo) hook:

```typescript
import {type BuildItemPresentationInfoCtx, connect, Item} from "datocms-plugin-sdk";

// A schema for our basic example
type ProductRecord = Item & {
  attributes: {
    name: 'string'
    product_type?: 'apple' | 'orange'
  }
}

// This checks to make sure an item is a product based on its API key, and if it is, assert that it is a ProductRecord
function isProductRecord(item: Item, ctx: BuildItemPresentationInfoCtx): item is ProductRecord {
  return ctx.itemTypes[item.relationships.item_type.data.id]?.attributes.api_key === 'product';
}

connect({
  async buildItemPresentationInfo(item: Item, ctx: BuildItemPresentationInfoCtx) {

    // We only want to override records in the `product` model
    if (!isProductRecord(item, ctx)) {
      return undefined; // Return undefined to let the record use its default values
    }

    // Get the record fields
    const {attributes: {product_type, name}} = item

    const fruitEmoji = {
      'apple': '🍎',
      'orange': '🍊',
      'unknown': '❓'
    }

    return {
      title: `${product_type ? fruitEmoji[product_type] : fruitEmoji['unknown']} ${name}`,
    }
  },
});
```

This level of flexibility empowers you to create a unique and tailored user experience that aligns with your goals.

The `buildItemPresentationInfo` hook can be used in numerous ways. For example, you can:

-   Combine multiple fields to present a record
-   Generate a preview image on the fly
    
-   Perform asynchronous API requests to third parties to compose the presentation
    

These are just a few examples of what you can do with the `buildItemPresentationInfo` hook. The possibilities are limitless, and you can use this hook to create the exact presentation you need.

The `buildItemPresentationInfo` hook is called every time a record needs to be presented, and it can return an object with `title` and/or `imageUrl` attributes, or `undefined`, if the plugin does not want to interfere with the default presentation at all.

> [!NOTE] imageUrl can also be a Data URL
> While the `imageUrl` attribute normally is a normal URL starting with `https://`, you can also pass a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs). Data URLs can be useful to generate an image on-the-fly in JavaScript (for example, [using canvases](https://davidwalsh.name/convert-image-data-uri-javascript)).

### Advanced Example: Data from an async fetch

Suppose that one of the models in a DatoCMS project is used to represent products in a ecommerce frontend, and that each product record in DatoCMS is linked to a particular Shopify product via its handle.

Shopify holds information like inventory availability, prices and variant images. We don't want to replicate the same information in DatoCMS, but it would be nice to show them inside the DatoCMS interface.

Since the `buildItemPresentationInfo` hook can be an async function, we can make a `fetch` call to the [Shopify Storefront API](https://shopify.dev/docs/api/storefront) (or any other API) and use its response in our collection display.

We'll modify our previous example to show use the result of this fetch instead, based on a new field `shopify_product_handle` (which holds an external ID) and a fake function `fetchShopifyProduct()` (simulating an external fetch):

(Image content)

```typescript
// Updated schema
type ProductRecord = Item & {
  attributes: {
    name: 'string'
    shopify_product_handle: string // A new required field
    // product_type?: 'apple' | 'orange' // No longer needed in the modified example
  }
}

// Updated hook
connect({
  async buildItemPresentationInfo(item: Item, ctx: BuildItemPresentationInfoCtx) {

    // Same function as before
    if (!isProductRecord(item, ctx)) {
      return undefined;
    }

    // Get the new field
    const {attributes: {name, shopify_product_handle}} = item

    // Just an example. In a real use case this would be an awaited fetch.
    const shopifyData = await fetchShopifyProduct(shopify_product_handle);

    const { imageUrl, availableForSale } = shopifyData;

    return {
      title: `${name} (${availableForSale ? '🛍️' : '🚫'})`,
      imageUrl,
    }
  },
})
```

The above is a simplified example using a fake fetch function. In a real project, to perform the actual API call to Shopify, we would need to implement a real fetch function using a real API token and the Shopify store domain. Both can be specified by the final user by [adding some settings to the plugin](/docs/plugin-sdk/config-screen.md).

A more realistic `fetchShopifyProduct` function might be something like this:

```typescript
import { Plugin } from "datocms-plugin-sdk";

type PluginParameters = {
  shopifyDomain: string;
  shopifyAccessToken: string;
}

async function fetchShopifyProduct(handle: string, plugin: Plugin) {
  const parameters = plugin.attributes.parameters as PluginParameters;

  const res = await fetch(
    `https://${parameters.shopifyDomain}.myshopify.com/api/graphql`,
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Shopify-Storefront-Access-Token': `${parameters.shopifyAccessToken}`,
      },
      body: JSON.stringify({
        query: `query getProduct($handle: String!) {
          product: productByHandle(handle: $handle) {
            title
            availableForSale
            images(first: 1) {
              edges {
                node {
                  src: transformedSrc(crop: CENTER, maxWidth: 200, maxHeight: 200)
                }
              }
            }
          }
        }`,
        variables: { handle },
      }),
    },
  );

  const body = await res.json();

  return {
    title: body.data.product.title,
    availableForSale: body.data.product.availableForSale,
    imageUrl: body.data.product.images.edges[0].node.src,
  };
}
```

#### `buildItemPresentationInfo(item: Item, ctx)`

Use this function to customize the presentation of a record in records collections and "Single link" or "Multiple links" field.

##### Return value

The function must return: `MaybePromise<ItemPresentationInfo | undefined>`.

##### Context object

The following properties and methods are available in the `ctx` argument:

---

# Plugin SDK — React UI Components

Source [docs]: https://www.datocms.com/docs/plugin-sdk/react-datocms-ui.md

If you're using React to build your plugin, you can take advantage of the `datocms-react-ui` package to get a library of ready-to-use components that are consistent with the UI of the main DatoCMS application. Using this library, you can create a custom interface for your plugin in a very short time.

## Wrap everything in Canvas!

When using the package it is required to wrap the content of your components in a `Canvas` component to apply the styling, and import the `styles.css` stylesheet:

```jsx
import { Canvas } from 'datocms-react-ui';
import 'datocms-react-ui/styles.css';

const MyComponent = ({ ctx }) => {
  return (
    <Canvas ctx={ctx}>
      Place your content here!
    </Canvas>
  );
}
```

The `Canvas` component needs the `ctx` object that is passed as an argument to all the hooks.

If you have a number of nested components below `MyComponent`, you don't need to pass the `ctx` around via props, as any component below `<Canvas>` can use the `useCtx` hook to retrieve it:

```jsx
import { Canvas, useCtx } from 'datocms-react-ui';

const MyComponent = ({ ctx }) => {
  return (
    <Canvas ctx={ctx}>
      <Inner />
    </Canvas>
  );
}

const Inner = () => {
  const ctx = useCtx();

  return (
    <div>Hi!</div>
  );
}
```

## Design tokens

Inside `Canvas`, the host (the DatoCMS app) exposes a full set of design tokens as CSS custom properties — colors, shadows, typography, and spacing. Components should reference these tokens directly: they adapt to the user's active theme (including dark mode) automatically.

### Colors

A full semantic color palette is exposed inside `Canvas` as `--color--*` CSS variables.

Regarding dark mode, `ctx.colorScheme` resolves to `'light'` or `'dark'`. The SDK runtime also sets `data-color-scheme` on `<html>` so selectors like `[data-color-scheme="dark"] {…}` work out of the box.

#### Token name shape

Tokens follow one of two name shapes:

| Shape | Meaning |
| --- | --- |
| `--color--{property}` | standalone (one `--` after color) |
| `--color--{context}--{property}` | context pair (two `--` after color) |

**Properties** are the role a color plays:

| Property | Role |
| --- | --- |
| `surface` | backgrounds |
| `ink` | text and icons |
| `border` | 1px lines |
| `outline` | focus rings and block-level rings |
| `fill` / `track` | indicator fills and their backgrounds |

**Standalone tokens** are for neutral page chrome; use them by default. Elevated neutral surfaces (modals, dropdowns, popovers) are standalone too, with hover and active variants for the raised layer. Pair them with the standalone ink tokens.

**Context tokens** describe a self-contained mini-environment (a primary CTA, a danger button). Contexts come in two shapes:

1.  **Ink-owning contexts**: signal contexts (primary, primary-soft, danger, danger-soft, warning-soft, success-soft, selected) and dark/inverted elevation contexts (overlay, backdrop, stacked, tooltip, code). Each defines an ink balanced on its own surface, so always pair surface and ink from the *same* context.
2.  **Single-property contexts**: focus (outline/border), progress (fill/track), highlight (surface), scrollbar (fill). Not surface+ink environments; the pairing rule doesn't apply.

> [!WARNING] Never cross ink-owning contexts
> Don't put a primary ink on a danger surface, or a danger-soft surface under a warning-soft ink. Each ink-owning context is contrast-balanced as a unit, and mixing produces illegible combinations, especially in dark mode.

#### Defining custom colors

Reserve custom colors for things genuinely outside the design system, such as brand illustrations, data-viz palettes, vendor-specific UI. Most needs ("primary button color", "error state") are already covered by tokens. When a custom color is justified, define it once per theme using the `[data-color-scheme="dark"]` selector that the SDK already sets:

```css
.my-plugin {
  --my-brand: #4a90e2;
}

[data-color-scheme="dark"] .my-plugin {
  --my-brand: #6aa9ec;
}

.my-plugin__cta {
  background: var(--my-brand);
  color: var(--color--primary--ink);
}
```

For non-CSS branching (image sources, third-party widget themes, syntax-highlighting presets), branch on `ctx.colorScheme` directly, e.g. `<img src={ctx.colorScheme === 'dark' ? logoDark : logoLight} />`. On modern browsers, the CSS `light-dark()` function is a more concise alternative to the per-theme variable pattern above.

#### Available tokens

A swatch for every available token, grouped by context.

Preview

Code

```js
<Canvas ctx={ctx}>
  <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-l)' }}>
    <StateManager initial={true}>
      {(isOpen, setOpen) => (
        <Section
          title="Standalone"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            One-level tokens that work on any neutral page. The <code>surface</code>, <code>ink</code> and <code>border</code> families cover the page background, body text and dividers; the <code>surface-raised</code> variants belong to the elevated layer used by modals, dropdowns and popovers. The tone-on-neutral inks (<code>ink-danger</code>, <code>ink-warning</code>, <code>ink-success</code>) color text and icons on a neutral surface; inside a toned panel use that context's own ink instead.
          </p>
          <Swatches
            tokens={[
              ['--color--surface', 'Page background everything else sits on'],
              ['--color--surface-hover', 'Hovered row inside lists and tables'],
              ['--color--surface-muted', 'Background of muted section panels and quiet cards'],
              ['--color--surface-raised', 'Elevated layer for modals, dropdowns and popovers'],
              ['--color--surface-raised-hover', 'Hovered option inside a dropdown menu'],
              ['--color--surface-raised-active', 'Focused or pressed option inside a dropdown menu'],
              ['--color--ink', 'Primary body text'],
              ['--color--ink-subtle', 'Secondary text, captions, helper labels'],
              ['--color--ink-hover', 'Toolbar icon and link fill on hover'],
              ['--color--ink-muted', 'Deemphasized text that should recede'],
              ['--color--ink-placeholder', 'Empty-input placeholder text'],
              ['--color--ink-primary', 'Theme-colored text and icons for branded labels'],
              ['--color--ink-link', 'Inline links and accent text'],
              ['--color--ink-danger', 'Error text or icon on a neutral surface'],
              ['--color--ink-warning', 'Warning text or icon on a neutral surface'],
              ['--color--ink-success', 'Success text or icon on a neutral surface'],
              ['--color--ink-disabled', 'Label color on disabled inputs and buttons'],
              ['--color--border', 'Default 1px divider between cards, rows and sections'],
              ['--color--border-hover', 'Border of an input or card when hovered'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Context: primary"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            The project's brand hue (the color the user picked for their DatoCMS project) at full strength. Reach for it on the main call-to-action button on a page, and on badges or navigation bars that need to stand out. The <code>surface-secondary</code> variant is a quieter brand surface step (for accent badges and inline action chips) that keeps the same white <code>ink</code>.
          </p>
          <Swatches
            tokens={[
              ['--color--primary--surface', 'Resting background of a primary call-to-action button'],
              ['--color--primary--surface-hover', 'Hovered primary button background'],
              ['--color--primary--surface-active', 'Pressed primary button background'],
              ['--color--primary--surface-muted', 'Muted variant of the primary surface'],
              ['--color--primary--surface-secondary', 'Quieter brand surface for accent badges and inline chips'],
              ['--color--primary--ink', 'Text and icon color sitting on any primary surface'],
              ['--color--primary--border', 'Thin border drawn on top of a primary surface'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Context: primary-soft"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            A soft panel in the same project brand hue: a pale brand surface paired with saturated brand ink. Quieter than primary, still clearly on-brand, for secondary actions, chips and tinted callouts.
          </p>
          <Swatches
            tokens={[
              ['--color--primary-soft--surface', 'Resting background of secondary brand-tinted buttons'],
              ['--color--primary-soft--surface-hover', 'Hovered tinted button background'],
              ['--color--primary-soft--surface-active', 'Pressed tinted button background'],
              ['--color--primary-soft--ink', 'Text and icon color on a soft brand surface'],
              ['--color--primary-soft--border', 'Thin border drawn on top of a soft brand surface'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Context: selected"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            The active selection state: the highlighted entry in a list or tree, the currently picked option in a radio or choice group, the chosen card in a gallery.
          </p>
          <Swatches
            tokens={[
              ['--color--selected--surface', 'Background of the currently active entry in a list or tree'],
              ['--color--selected--surface-hover', 'Hover on an entry that is already selected'],
              ['--color--selected--ink', 'Text and icon color inside the selected entry'],
              ['--color--selected--border', 'Border or outline ring drawn around a selected option or card'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Context: disabled"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            The flat, low-contrast pair applied to non-interactive controls: disabled buttons, disabled selects and disabled toggles.
          </p>
          <PairSwatches
            tokens={[
              ['--color--disabled--surface', '--color--disabled--ink', 'Disabled button or control: muted background with low-contrast label'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Context: danger"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Reserved for destructive actions, such as Delete, Remove or Reset operations.
          </p>
          <PairSwatches
            tokens={[
              ['--color--danger--surface', '--color--danger--ink', 'Destructive action button: vivid red surface with white label'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Context: focus"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            The keyboard-focus ring drawn around inputs, buttons and any other focusable control. Pair <code>border</code> on the element itself with <code>outline</code> as a soft halo.
          </p>
          <Swatches
            tokens={[
              ['--color--focus--border', 'Border color of the focused element'],
              ['--color--focus--outline', 'Soft outline ring drawn around the focused element'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Signal tones"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Soft panels for validation states and notifications: red for failures, amber for warnings, green for successes. Each <code>-soft</code> context is a pale surface paired with saturated ink, following the same four-property shape (surface, ink, border, outline), so you can swap the tone without touching layout. For a colored message on a plain neutral surface, reach for the standalone <code>ink-danger</code>, <code>ink-warning</code> and <code>ink-success</code> instead.
          </p>
          <Swatches
            tokens={[
              ['--color--danger-soft--surface', 'Background of error banners and alert toasts'],
              ['--color--danger-soft--ink', 'Error message text and the icon inside an error panel'],
              ['--color--danger-soft--border', 'Border around an invalid input or alert toast'],
              ['--color--danger-soft--outline', 'Soft halo around an invalid field on focus'],
              ['--color--warning-soft--surface', 'Background of warning banners and plugin notices'],
              ['--color--warning-soft--ink', 'Text inside warning banners and warning toasts'],
              ['--color--warning-soft--border', 'Border around warning banners and modified-state pills'],
              ['--color--warning-soft--outline', 'Soft halo for warning emphasis'],
              ['--color--success-soft--surface', 'Background of success toasts'],
              ['--color--success-soft--ink', 'Text inside success toasts and success banners'],
              ['--color--success-soft--border', 'Border around success banners'],
              ['--color--success-soft--outline', 'Soft halo for success emphasis'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Context: highlight"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            The yellow marker pen for inline rich-text highlights inside Structured Text editors.
          </p>
          <Swatches
            tokens={[
              ['--color--highlight--surface', 'Background of a highlighted span inside a rich text editor'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Diffs"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Content-versioning palette across three intents: green for added, red for removed, blue for changed. Inline text diffs use the surface tint; block-level revision panels use the outline. For positive/negative rule indicators, the left-border tone depends on whether the rule was just edited: a subtle ink when stable, a vivid one when freshly changed. The changed variant has no ink stops, since rule borders are only ever green or red.
          </p>
          <Swatches
            tokens={[
              ['--color--diff-added--surface', 'Background of inline added text inside a text diff'],
              ['--color--diff-added--outline', 'Outline drawn around a block-level added revision panel'],
              ['--color--diff-added--ink', 'Left-border color of a positive rule when it was recently changed (vivid)'],
              ['--color--diff-added--ink-subtle', 'Left-border color of a positive rule when it was not recently changed'],
              ['--color--diff-removed--surface', 'Background of inline removed text inside a text diff'],
              ['--color--diff-removed--outline', 'Outline drawn around a block-level removed revision panel'],
              ['--color--diff-removed--ink', 'Left-border color of a negative rule when it was recently changed (vivid)'],
              ['--color--diff-removed--ink-subtle', 'Left-border color of a negative rule when it was not recently changed'],
              ['--color--diff-changed--surface', 'Background of inline changed text inside a text diff'],
              ['--color--diff-changed--outline', 'Outline drawn around a block-level changed revision panel'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Status"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Publishing-workflow status dots. Ink-only because the colored dot is the whole marker, no surface or border needed.
          </p>
          <Swatches
            tokens={[
              ['--color--status-draft--ink', 'Dot color for records that exist only as a draft'],
              ['--color--status-outdated--ink', 'Dot color for published records with unpublished changes'],
              ['--color--status-published--ink', 'Dot color for fully published records'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Backdrop and overlay"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Two scrims for two jobs. The backdrop is the full-screen dim painted behind a modal dialog. The overlay is the lighter scrim that sits on top of media or thumbnails and hosts reversed buttons designed to read against dark imagery.
          </p>
          <PairSwatches
            tokens={[
              ['--color--backdrop--surface', '--color--backdrop--ink', 'Full-screen modal dim with icon color for close controls'],
            ]}
          />
          <Swatches
            tokens={[
              ['--color--overlay--surface', 'Scrim painted over media thumbnails and image cards'],
              ['--color--overlay--surface-hover', 'Hover background of a reversed button floating on dark media'],
              ['--color--overlay--surface-active', 'Pressed background of a reversed button on dark media'],
              ['--color--overlay--ink', 'Text and icon color inside reversed buttons on overlay surfaces'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Stacked"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Layered dark inline areas, the kind used for asset uploaders and audio/video players. The wrapper paints the base surface; an inner detail panel sits a layer up; transparent action buttons gain visibility on hover and press.
          </p>
          <Swatches
            tokens={[
              ['--color--stacked--surface', 'Base layer of a dark inline panel'],
              ['--color--stacked--surface-upper', 'Inner detail panel sitting one layer above the base'],
              ['--color--stacked--surface-action', 'Resting background of action buttons inside a stacked panel (transparent)'],
              ['--color--stacked--surface-action-hover', 'Hovered action button inside a stacked panel'],
              ['--color--stacked--surface-action-active', 'Pressed action button inside a stacked panel'],
              ['--color--stacked--ink', 'Main text and values on a stacked surface'],
              ['--color--stacked--ink-subtle', 'Field labels and secondary text on a stacked surface'],
              ['--color--stacked--border', 'Column rules and dividers inside a stacked panel'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Progress"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Horizontal progress bars used to report quota usage, upload advancement and similar percentage indicators.
          </p>
          <Swatches
            tokens={[
              ['--color--progress--track', 'Empty portion of the bar (the background track)'],
              ['--color--progress--fill', 'Filled portion of the bar, drawn in the brand color'],
              ['--color--progress--fill-hover', 'Fill color when the bar is hovered'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Tooltip"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Small dark floating labels: the plain tooltip shown on hover, and the keyboard-hint variant that pairs a description with a keyboard shortcut.
          </p>
          <Swatches
            tokens={[
              ['--color--tooltip--surface', 'Background of standard and keyboard-hint tooltips'],
              ['--color--tooltip--surface-hover', 'Hover background for interactive controls living inside a tooltip'],
              ['--color--tooltip--ink', 'Primary text inside a tooltip'],
              ['--color--tooltip--ink-subtle', 'Secondary text inside a tooltip, e.g. the keyboard shortcut hint'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Code"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            The dark monospaced surface used by build logs, error traces and other terminal-style output.
          </p>
          <PairSwatches
            tokens={[
              ['--color--code--surface', '--color--code--ink', 'Dark monospaced surface with its text color'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Scrollbar"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Tint applied globally to the native scrollbar thumb. Most visible in Firefox and on systems that keep scrollbars always on.
          </p>
          <Swatches
            tokens={[
              ['--color--scrollbar--fill', 'Color of the native scrollbar thumb across the whole app'],
            ]}
          />
        </Section>
      )}
    </StateManager>

    <StateManager initial={false}>
      {(isOpen, setOpen) => (
        <Section
          title="Field type groups"
          collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
        >
          <p>
            Fixed-hue soft chips for field type group icons. Each context exposes a <code>surface</code> (chip background) and an <code>ink</code> (icon fill). The hues are not brand-adaptive — they are fixed across projects and automatically flip between a pale surface with saturated ink in light mode and a deep surface with bright ink in dark mode.
          </p>
          <PairSwatches
            tokens={[
              ['--color--field-group-text--surface', '--color--field-group-text--ink', 'Text / string / structured-text fields'],
              ['--color--field-group-rich-text--surface', '--color--field-group-rich-text--ink', 'Rich-text and single-block fields'],
              ['--color--field-group-media--surface', '--color--field-group-media--ink', 'File, gallery and video fields'],
              ['--color--field-group-datetime--surface', '--color--field-group-datetime--ink', 'Date and date-time fields'],
              ['--color--field-group-number--surface', '--color--field-group-number--ink', 'Integer and float fields'],
              ['--color--field-group-boolean--surface', '--color--field-group-boolean--ink', 'Boolean fields'],
              ['--color--field-group-location--surface', '--color--field-group-location--ink', 'Lat/lon fields'],
              ['--color--field-group-color--surface', '--color--field-group-color--ink', 'Color fields'],
              ['--color--field-group-seo--surface', '--color--field-group-seo--ink', 'Slug and SEO fields'],
              ['--color--field-group-reference--surface', '--color--field-group-reference--ink', 'Link and links fields'],
              ['--color--field-group-json--surface', '--color--field-group-json--ink', 'JSON fields'],
            ]}
          />
        </Section>
      )}
    </StateManager>
  </div>
</Canvas>
```

### Shadows

Four ready-made `box-shadow` composites (raised, floating, lifted, ambient). Drop them straight into a `box-shadow` property.

Preview

Code

```js
<Canvas ctx={ctx}>
  <Swatches
    kind="shadow"
    tokens={['--shadow--raised', '--shadow--floating', '--shadow--lifted', '--shadow--ambient']}
  />
</Canvas>
```

### Typography

Typography is a foundational element in UI design. Good typography establishes a strong, cohesive visual hierarchy and presents content clearly and efficiently to users. Within the `Canvas` component, a set of CSS variables is available allowing your plugin to conform to the overall look&feel of DatoCMS:

Preview

Code

```js
<Canvas ctx={ctx}>
  <Swatches
    kind="font-size"
    tokens={[
      '--font-size-xxs',
      '--font-size-xs',
      '--font-size-s',
      '--font-size-m',
      '--font-size-l',
      '--font-size-xl',
      '--font-size-xxl',
      '--font-size-xxxl',
    ]}
  />
</Canvas>
```

### Spacing

The following CSS variables are available as well, to mimick the spacing between elements used by the main DatoCMS application. Negative spacing variables are available too (`--negative-spacing-<SIZE>`).

Preview

Code

```js
<Canvas ctx={ctx}>
  <Spacings
    tokens={[
      '--spacing-s',
      '--spacing-m',
      '--spacing-l',
      '--spacing-xl',
      '--spacing-xxl',
      '--spacing-xxxl',
    ]}
  />
</Canvas>
```

---

# Plugin SDK — Button

Source [docs]: https://www.datocms.com/docs/plugin-sdk/button.md

Buttons communicate the action that will occur when the user clicks them. They communicate calls to action to the user and allow users to interact with pages in a variety of ways. They contain a text label to describe the action, and an icon if appropriate.

Available variations:

-   **Primary**: used for the most important actions in any scenario. Don’t use more than one primary button in a section or screen to avoid overwhelming users
-   **Muted**: used for a secondary actions, the most commonly used button type
    
-   **Negative**: for destructive actions - when something can't be undone. For example, deleting entities
    

### Button types

Preview

Code

```js
<Canvas ctx={ctx}>
  <div style={{ marginBottom: 'var(--spacing-m)' }}>
    <Button buttonType="muted">Submit</Button>{' '}
    <Button buttonType="primary">Submit</Button>{' '}
    <Button buttonType="negative">Submit</Button>
  </div>
  <div>
    <Button buttonType="muted" disabled>
      Submit
    </Button>{' '}
    <Button buttonType="primary" disabled>
      Submit
    </Button>{' '}
    <Button buttonType="negative" disabled>
      Submit
    </Button>
  </div>
</Canvas>
```

### Full-width

Preview

Code

```js
<Canvas ctx={ctx}>
  <Button fullWidth>Submit</Button>
</Canvas>
```

### Sizing

Preview

Code

```js
<Canvas ctx={ctx}>
  <Button buttonSize="xxs">Submit</Button>{' '}
  <Button buttonSize="xs">Submit</Button>{' '}
  <Button buttonSize="s">Submit</Button>{' '}
  <Button buttonSize="m">Submit</Button>{' '}
  <Button buttonSize="l">Submit</Button>{' '}
  <Button buttonSize="xl">Submit</Button>{' '}
</Canvas>
```

### Icons

Preview

Code

```js
<Canvas ctx={ctx}>
  <div style={{ marginBottom: 'var(--spacing-m)' }}>
    <Button leftIcon={<PlusIcon />}>Submit</Button>
  </div>
  <div style={{ marginBottom: 'var(--spacing-m)' }}>
    <Button rightIcon={<ChevronDownIcon />}>Options</Button>
  </div>
  <div>
    <Button leftIcon={<PlusIcon />} />
  </div>
</Canvas>
```

---

# Plugin SDK — Button group

Source [docs]: https://www.datocms.com/docs/plugin-sdk/button-group.md

### Basic example

Preview

Code

```js
<Canvas ctx={ctx}>
  <ButtonGroup>
    <ButtonGroupButton>First</ButtonGroupButton>
    <ButtonGroupButton selected>Second</ButtonGroupButton>
    <ButtonGroupButton>Third</ButtonGroupButton>
    <ButtonGroupButton disabled>Fourth</ButtonGroupButton>
  </ButtonGroup>
</Canvas>
```

---

# Plugin SDK — Dropdown

Source [docs]: https://www.datocms.com/docs/plugin-sdk/dropdown.md

### Basic example

Preview

Code

```js
<Canvas ctx={ctx}>
  <Dropdown
    renderTrigger={({ open, onClick }) => (
      <Button
        onClick={onClick}
        rightIcon={open ? <CaretUpIcon /> : <CaretDownIcon />}
      >
        Options
      </Button>
    )}
  >
    <DropdownMenu>
      <DropdownOption onClick={() => {}}>Edit</DropdownOption>
      <DropdownOption disabled onClick={() => {}}>
        Duplicate
      </DropdownOption>
      <DropdownSeparator />
      <DropdownOption red onClick={() => {}}>
        Delete
      </DropdownOption>
    </DropdownMenu>
  </Dropdown>
</Canvas>
```

### Option actions

Preview

Code

```js
<Canvas ctx={ctx}>
  <Dropdown
    renderTrigger={({ open, onClick }) => (
      <Button
        onClick={onClick}
        rightIcon={open ? <CaretUpIcon /> : <CaretDownIcon />}
      >
        Fields
      </Button>
    )}
  >
    <DropdownMenu>
      <DropdownOption>
        First option
        <DropdownOptionAction icon={<PlusIcon />} onClick={() => {}} />
        <DropdownOptionAction
          red
          icon={<TrashIcon />}
          onClick={() => {}}
        />
      </DropdownOption>
      <DropdownOption>
        Second option
        <DropdownOptionAction icon={<PlusIcon />} onClick={() => {}} />
        <DropdownOptionAction
          red
          icon={<TrashIcon />}
          onClick={() => {}}
        />
      </DropdownOption>
    </DropdownMenu>
  </Dropdown>
</Canvas>
```

### Option groups

Preview

Code

```js
<Canvas ctx={ctx}>
  <Dropdown
    renderTrigger={({ open, onClick }) => (
      <Button
        onClick={onClick}
        rightIcon={open ? <CaretUpIcon /> : <CaretDownIcon />}
      >
        Fields
      </Button>
    )}
  >
    <DropdownMenu>
      <DropdownGroup name="Group 1">
        <DropdownOption>Foo</DropdownOption>
        <DropdownOption>Bar</DropdownOption>
        <DropdownOption>Qux</DropdownOption>
      </DropdownGroup>
      <DropdownGroup name="Group 2">
        <DropdownOption>Foo</DropdownOption>
        <DropdownOption>Bar</DropdownOption>
        <DropdownOption>Qux</DropdownOption>
      </DropdownGroup>
      <DropdownGroup name="Group 3">
        <DropdownOption>Foo</DropdownOption>
        <DropdownOption>Bar</DropdownOption>
        <DropdownOption>Qux</DropdownOption>
      </DropdownGroup>
    </DropdownMenu>
  </Dropdown>
</Canvas>
```

---

# Plugin SDK — Form

Source [docs]: https://www.datocms.com/docs/plugin-sdk/form.md

The `Form` component should wrap `FieldGroup` components to apply consistent layouts. All the fields are controlled inputs, so you need to provide both `value` and `onChange` props to make it work.

The `onChange` prop of all field components always returns the new value as first parameter, so you don't need to inspect the `event` object to get it.

### Full example

Preview

Code

```js
<Canvas ctx={ctx}>
  <Form onSubmit={() => console.log('onSubmit')}>
    <FieldGroup>
      <TextField
        required
        name="name"
        id="name"
        label="Name"
        value="Mark Smith"
        placeholder="Enter full name"
        hint="Provide a full name"
        onChange={(newValue) => console.log(newValue)}
      />
      <TextField
        required
        name="email"
        id="email"
        label="Email"
        type="email"
        value=""
        placeholder="your@email.com"
        error="Please enter an email!"
        hint="Enter email address"
        onChange={(newValue) => console.log(newValue)}
      />
      <TextField
        required
        name="apiToken"
        id="apiToken"
        label="API Token"
        value="XXXYYY123"
        hint="Enter a valid API token"
        textInputProps={{ monospaced: true }}
        onChange={(newValue) => console.log(newValue)}
      />
      <TextareaField
        required
        name="longText"
        id="longText"
        label="Long text"
        value="Lorem ipsum dolor sit amet, consectetur adipiscing elit.."
        hint="Enter some text"
        onChange={(newValue) => console.log(newValue)}
      />
      <SelectField
        name="option"
        id="option"
        label="Option"
        hint="Select one of the options"
        value={{ label: 'Option 1', value: 'option1' }}
        selectInputProps={{
          options: [
            { label: 'Option 1', value: 'option1' },
            { label: 'Option 2', value: 'option2' },
            { label: 'Option 3', value: 'option3' },
          ],
        }}
        onChange={(newValue) => console.log(newValue)}
      />
      <SelectField
        name="multipleOption"
        id="multipleOption"
        label="Multiple options"
        hint="Select one of the options"
        value={[
          { label: 'Option 1', value: 'option1' },
          { label: 'Option 2', value: 'option2' },
        ]}
        selectInputProps={{
          isMulti: true,
          options: [
            { label: 'Option 1', value: 'option1' },
            { label: 'Option 2', value: 'option2' },
            { label: 'Option 3', value: 'option3' },
          ],
        }}
        onChange={(newValue) => console.log(newValue)}
      />
      <SwitchField
        name="debugMode"
        id="debugMode"
        label="Debug mode active?"
        hint="Logs messages to console"
        value={true}
        onChange={(newValue) => console.log(newValue)}
      />
    </FieldGroup>
    <FieldGroup>
      <Button fullWidth buttonType="primary">
        Submit
      </Button>
    </FieldGroup>
  </Form>
</Canvas>
```

---

# Plugin SDK — Section

Source [docs]: https://www.datocms.com/docs/plugin-sdk/section.md

### Basic usage

Preview

Code

```js
<Canvas ctx={ctx}>
  <Section title="Section title">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
    eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
    ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    aliquip ex ea commodo consequat.
  </Section>
</Canvas>
```

### Highlighted

Preview

Code

```js
<Canvas ctx={ctx}>
  <Section title="Section title" highlighted>
    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
    eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
    ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
    aliquip ex ea commodo consequat.
  </Section>
</Canvas>
```

### Collapsible

Preview

Code

```js
<Canvas ctx={ctx}>
  <StateManager initial={true}>
    {(isOpen, setOpen) => (
      <Section
        title="Section title"
        collapsible={{ isOpen, onToggle: () => setOpen((v) => !v) }}
      >
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
        eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
        enim ad minim veniam, quis nostrud exercitation ullamco laboris
        nisi ut aliquip ex ea commodo consequat.
      </Section>
    )}
  </StateManager>
</Canvas>
```

---

# Plugin SDK — Sidebar panel

Source [docs]: https://www.datocms.com/docs/plugin-sdk/sidebar-panel.md

### Basic example

Preview

Code

```js
<Canvas ctx={ctx}>
  <div style={{ display: 'flex' }}>
    <div
      style={{
        width: '300px',
        borderRight: '1px solid var(--color--border)',
      }}
    >
      <SidebarPanel title="Default">Content</SidebarPanel>
      <SidebarPanel title="Start open" startOpen>
        Content
      </SidebarPanel>
      <SidebarPanel title="Content with no padding" noPadding>
        Content
      </SidebarPanel>
    </div>
    <div
      style={{
        flex: '1',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        background: 'var(--color--surface-muted)',
      }}
    >
      Main content
    </div>
  </div>
</Canvas>
```

---

# Plugin SDK — Spinner

Source [docs]: https://www.datocms.com/docs/plugin-sdk/spinner.md

### Inline spinner

Preview

Code

```js
<Canvas ctx={ctx}>
  Foo bar <Spinner size={24} />
</Canvas>
```

### Centered spinner

Preview

Code

```js
<Canvas ctx={ctx}>
  <div style={{ height: '200px', position: 'relative' }}>
    <Spinner size={48} placement="centered" />
  </div>
</Canvas>
```

---

# Plugin SDK — Toolbar

Source [docs]: https://www.datocms.com/docs/plugin-sdk/toolbar.md

### Basic example

Preview

Code

```js
<Canvas ctx={ctx}>
  <Toolbar>
    <ToolbarStack stackSize="l">
      <ToolbarTitle>Media Area</ToolbarTitle>
    </ToolbarStack>
  </Toolbar>
  <div
    style={{
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      background: 'var(--color--surface-muted)',
      height: '150px',
    }}
  >
    Main content
  </div>
</Canvas>
```

### Buttons and actions

Preview

Code

```js
<Canvas ctx={ctx}>
  <Toolbar>
    <ToolbarButton>
      <BackIcon />
    </ToolbarButton>
    <ToolbarStack stackSize="l">
      <ToolbarTitle>Media Area</ToolbarTitle>
      <div style={{ flex: '1' }} />
      <Button buttonType="primary">Action</Button>
    </ToolbarStack>
    <ToolbarButton>
      <SidebarLeftArrowIcon />
    </ToolbarButton>
  </Toolbar>
  <div
    style={{
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      background: 'var(--color--surface-muted)',
      height: '150px',
    }}
  >
    Main content
  </div>
</Canvas>
```

### With button group

Preview

Code

```js
<Canvas ctx={ctx}>
  <Toolbar>
    <ToolbarStack stackSize="l">
      <ToolbarTitle>Media Area</ToolbarTitle>
      <div style={{ flex: '1' }} />
      <ButtonGroup>
        <ButtonGroupButton>First</ButtonGroupButton>
        <ButtonGroupButton selected>Second</ButtonGroupButton>
        <ButtonGroupButton>Third</ButtonGroupButton>
      </ButtonGroup>
    </ToolbarStack>
  </Toolbar>
  <div
    style={{
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      background: 'var(--color--surface-muted)',
      height: '150px',
    }}
  >
    Main content
  </div>
</Canvas>
```

---

# Plugin SDK — Sidebars and split views

Source [docs]: https://www.datocms.com/docs/plugin-sdk/sidebars-and-split-views.md

### Resizable, left primary panel

Preview

Code

```js
<Canvas ctx={ctx}>
  <div style={{ height: 500, position: 'relative' }}>
    <VerticalSplit primaryPane="left" size="25%" minSize={220}>
      <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
        <Toolbar>
          <ToolbarStack stackSize="l">
            <ToolbarTitle>Primary</ToolbarTitle>
          </ToolbarStack>
        </Toolbar>
        <div
          style={{
            flex: '1',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            height: '150px',
          }}
        >
          Main content
        </div>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', height: '100%', borderLeft: '1px solid var(--color--border)' }}>
        <Toolbar>
          <ToolbarStack stackSize="l">
            <ToolbarTitle>Secondary</ToolbarTitle>
          </ToolbarStack>
        </Toolbar>
        <div
          style={{
            flex: '1',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            height: '150px',
          }}
        >
          Sidebar
        </div>
      </div>
    </VerticalSplit>
  </div>
</Canvas>
```

### Resizable, right primary panel

Preview

Code

```js
<Canvas ctx={ctx}>
  <div style={{ height: 500, position: 'relative' }}>
    <VerticalSplit primaryPane="right" size="25%" minSize={220}>
      <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
        <Toolbar>
          <ToolbarStack stackSize="l">
            <ToolbarTitle>Secondary</ToolbarTitle>
          </ToolbarStack>
        </Toolbar>
        <div
          style={{
            flex: '1',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            height: '150px',
          }}
        >
          Sidebar
        </div>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', height: '100%', borderLeft: '1px solid var(--color--border)' }}>
        <Toolbar>
          <ToolbarStack stackSize="l">
            <ToolbarTitle>Primary</ToolbarTitle>
          </ToolbarStack>
        </Toolbar>
        <div
          style={{
            flex: '1',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            height: '150px',
          }}
        >
          Main content
        </div>
      </div>
    </VerticalSplit>
  </div>
</Canvas>
```

### Collapsible

Preview

Code

```js
  <Canvas ctx={ctx}>
   <div style={{ height: 500, position: 'relative' }}>
     <StateManager initial={true}>
       {(isCollapsed, setCollapsed) => (
         <VerticalSplit
           primaryPane="left"
           size="25%"
           minSize={220}
           isSecondaryCollapsed={isCollapsed}
           onSecondaryToggle={setCollapsed}
         >
           <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
             <Toolbar>
               <ToolbarStack stackSize="l">
                 <ToolbarTitle>Primary</ToolbarTitle>
               </ToolbarStack>
             </Toolbar>
             <div
               style={{
                 flex: '1',
                 display: 'flex',
                 justifyContent: 'center',
                 alignItems: 'center',
                 height: '150px',
               }}
             >
               Main content
             </div>
           </div>
           <div
             style={{
               display: 'flex',
               flexDirection: 'column',
               height: '100%',
               borderLeft: '1px solid var(--color--border)',
             }}
           >
             <Toolbar>
               <ToolbarStack stackSize="l">
                 <ToolbarTitle>Secondary</ToolbarTitle>
               </ToolbarStack>
             </Toolbar>
             <div
               style={{
                 flex: '1',
                 display: 'flex',
                 justifyContent: 'center',
                 alignItems: 'center',
                 height: '150px',
               }}
             >
               Sidebar
             </div>
           </div>
         </VerticalSplit>
       )}
     </StateManager>
   </div>
 </Canvas>
```

### Overlay mode

Preview

Code

```js
  <Canvas ctx={ctx}>
   <div style={{ height: 500, position: 'relative' }}>
     <StateManager initial={true}>
       {(isCollapsed, setCollapsed) => (
         <VerticalSplit
           mode="overlay"
           primaryPane="left"
           size="25%"
           minSize={220}
           isSecondaryCollapsed={isCollapsed}
           onSecondaryToggle={setCollapsed}
         >
           <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
             <Toolbar>
               <ToolbarStack stackSize="l">
                 <ToolbarTitle>Primary</ToolbarTitle>
               </ToolbarStack>
             </Toolbar>
             <div
               style={{
                 flex: '1',
                 display: 'flex',
                 justifyContent: 'center',
                 alignItems: 'center',
                 height: '150px',
               }}
             >
               Main content
             </div>
           </div>
           <div
             style={{
               display: 'flex',
               flexDirection: 'column',
               height: '100%',
               borderLeft: '1px solid var(--color--border)',
             }}
           >
             <Toolbar>
               <ToolbarStack stackSize="l">
                 <ToolbarTitle>Secondary</ToolbarTitle>
               </ToolbarStack>
             </Toolbar>
             <div
               style={{
                 flex: '1',
                 display: 'flex',
                 justifyContent: 'center',
                 alignItems: 'center',
                 height: '150px',
               }}
             >
               Sidebar
             </div>
           </div>
         </VerticalSplit>
       )}
     </StateManager>
   </div>
 </Canvas>
```

---

# Plugin SDK — Additional permissions

Source [docs]: https://www.datocms.com/docs/plugin-sdk/additional-permissions.md

Some methods and properties available within the hooks require special permissions to be accessed, as they may cause security issues.

If a plugin wants to access these additional features, it must request specific permissions. When installing the plugin, the user must explicitly grant these permissions, otherwise the installation process will be aborted:

(Image content)

# Available permissions

At the moment, only one special permit is available, but in the future more may be added.

### `currentUserAccessToken`

This permission makes the `ctx.currentUserAccessToken` property available. This token represents the currently logged in user, and you can use it to make API calls to the [Content Management API](/docs/content-management-api/using-the-nodejs-clients.md) on behalf of that user.

```jsx
import { SiteClient } from 'datocms-client';
import { useMemo, useEffect } from 'react';

connect({
  renderPage(pageId, { ctx }) {
    const client = useMemo(() => {
      return new SiteClient(
        ctx.currentUserAccessToken,
        { environment: ctx.environment },
      );
    }, [ctx.currentUserAccessToken]);

    useEffect(async () => {
      const someRecords = await client.items.all();
    }, []);

    // ...
  },
});
```

## Specifying additional permissions

#### Private plugins

During the creation of a plugin, it is possible to specify the additional permissions the plugin requires:

(Video content)

#### Marketplace plugins

Public plugins must declare their additional permissions inside the `datocmsPlugin.permission` key in their `package.json` file:

```json
{
  "name": "datocms-plugin-foobar",
  "version": "0.1.0",
  "dependencies": {
    // ...
  },
  "datoCmsPlugin": {
    "title": "Foobar",
    // ...
    "permissions": ["currentUserAccessToken"]
  }
}
```

For more information regarding how to publish a plugin in the Marketplace, see [here](/docs/plugin-sdk/publishing-to-marketplace.md).

---

# Plugin SDK — Working with form values

Source [docs]: https://www.datocms.com/docs/plugin-sdk/working-with-form-values.md

Inside of [Sidebar panels](/docs/plugin-sdk/sidebar-panels.md) and [Field extensions](/docs/plugin-sdk/field-extensions.md) you have access to `ctx.formValues`, which contains the complete internal form state for the record that the current user is editing. With that, you can access its work-in-progress changes, and react to them.

The structure of `ctx.formValues` is heavily dependent on the fields of its model. In fact, the keys of this object are the model's field IDs:

```json
{
  "title": "Foo bar",
  "cover_image": {
    "upload_id": "32943530"
    "alt": null,
    "title": null,
    "focal_point": null,
    "custom_data": {},
  },
  "author": "39832254",
  "seo": {
    "image": "16229550",
    "title": "Hugo",
    "description": "With Hugo, you can build amazing static projects",
    "twitter_card": "summary"
  },
}
```

If you want to change the value of some field, you can use the `ctx.setFieldValue` method:

```typescript
await ctx.setFieldValue('title', 'new value');
```

Most of the field values you'll find are 100% identical to their respective Content Management API formats (see the section ["Field type values"](/docs/content-management-api/resources/item/create.md)), even tough there are a couple of important exceptions we'll cover below.

## Localized fields

If a field is localized, the format of `ctx.formValues` will slightly change, similarly to what happens on the Content Management API (see the section ["Localized fields"](/docs/content-management-api/resources/item/create.md)):

```json
{
  "title": {
    "en": "Foo bar",
    "it": "Antani"
  }
}
```

In this case, to change the field value in English, you need to pass the complete field path to `ctx.setFieldValue`:

```typescript
await ctx.setFieldValue('title.en', 'new value');
```

## Modular Content fields

As you know, modular content fields contain [blocks](/docs/content-modelling/blocks.md), which are complex structures composed of multiple inner fields. If you inspect the value of a modular content field from `ctx.formValues`, you'll see something like this:

```json
[
  {
    "itemId": "39830695",
    "itemTypeId": "810886",
    "social": "twitter",
    "url": "https://twitter.com/datocms",
  },
  {
    "itemId": "39830696",
    "itemTypeId": "810886",
    "social": "linkedin",
    "url": "https://www.linkedin.com/company/35537033"
  }
]
```

Every block contains the `itemId` (ID of the block) and `itemTypeId` (ID of the block model) attributes, while all the other attributes depend on the actual fields of the block model.

You can edit the value of a Modular Content field just like any other field. Following the example above, you could ie. reorder the existing blocks by social using the `ctx.setFieldValue` method:

```typescript
const currentValue = ctx.formValues['my_modular_content'];

await ctx.setFieldValue(
  'my_modular_content',
  currentValue.sort((a, b) => a.social.localeCompare(b.social),
);
```

But you can also remove some blocks:

```typescript
await ctx.setFieldValue(
  'my_modular_content',
  currentValue.filter(block => block.social !== 'linkedin'),
);
```

Or even add new blocks to the field:

```typescript
await ctx.setFieldValue(
  'my_modular_content',
  [
    ...currentValue,
    {
      "itemTypeId": "810886",
      "social": "twitter",
      "url": "https://twitter.com/datocms",
    },
  ],
);
```

Pay attention to the missing `itemId` attribute here: when the record will be eventually saved, a new `itemId` will be generated by the DatoCMS API.

> [!WARNING] Avoid creating Editor field extensions for Modular Content fields!
> While it's perfectly fine — and as we just saw, quite straightforward — to develop Addon field extensions for Modular Content fields, overriding the regular editor DatoCMS offers for this field type is generally not a good idea, as you'll need to handle the rendering and update of all the fields and blocks it contains. Not an easy task.

## Field extensions on block fields

If a Field Extension is installed on a field belonging to a block, nothing really changes. You can get the value of the specific field of the block using `ctx.fieldPath`:

```typescript
import get from 'lodash-es/get';

// ctx.fieldPath for a block field will be something
// like "my_modular_content.1.title"
get(ctx.formValues, ctx.fieldPath);
```

## Structured Text fields

If you inspect the value of a Structured Text field from `ctx.formValues`, you'll see something like this:

```json
{
  "my_structured_text_field": [
    {
      "type": "paragraph",
      "children": [
        {
          "text": "Meet "
        },
        {
          "text": "the best way",
          "highlight": true
        },
        {
          "text": " to manage content with Hugo"
        }
      ]
    }
  ]
}
```

Even with this tiny one-paragraph example, you'll notice that this format is quite different from the [`dast` format](/docs/structured-text/dast.md) that both CMA and CDA offers:

-   There's no [`root`](/docs/structured-text/dast.md#root) node: the value is directly an array of root children;
-   Nodes of type [`span`](/docs/structured-text/dast.md#span) have no `type` attribute, the `value` attribute is called `text`, and `marks` are applied as boolean keys directly on the node itself.
    

To offer a comparison, this would be the `dast` version of the same content:

```json
{
  "my_structured_text_field": {
    "schema": "dast",
    "document": {
      "type": "root",
      "children": [
        {
          "type": "paragraph",
          "children": [
            {
              "type": "span",
              "value": "Meet "
            },
            {
              "type": "span",
              "marks": [
                "highlight"
              ],
              "value": "the best way"
            },
            {
              "type": "span",
              "value": " to manage content with Hugo"
            }
          ]
        }
      ]
    }
  }
}
```

Why is that? Because to power Structured Text fields, under the hood, the DatoCMS application uses the (awesome) [Slate Editor](https://github.com/ianstormtaylor/slate) library. Its [internal representation format](https://docs.slatejs.org/concepts/02-nodes) is somewhat different from `dast`, and continuously converting back-and-forth from the two formats on every key stroke was infeasible from a performance point of view.

So, how can you overcome this constraint?

If your plugin just needs to read Structured Text fields, without ever changing their value, you can use the `slateToDast` function exposed by the `datocms-structured-text-slate-utils` package to convert the internal Slate format into regular `dast`, and then do your reading on its result:

```typescript
import { slateToDast } from 'datocms-structured-text-slate-utils';
import groupBy from 'lodash-es/groupBy';

const allFieldsByItemTypeId = groupBy(
  Object.values(ctx.fields), field => field.relationships.item_type.data.id
);

const dast = slateToDast(
  ctx.formValues['my_structured_text_field'],
  allFieldsByItemTypeId,
);

// result will be something like:
//
// {
//    schema: 'dast',
//    document: { type: 'root', children: [...] },
//  }
```

If you want to read AND write the content of a Structured Text field, then the `datocms-structured-text-slate-utils` package offers [complete Typescript types](https://github.com/datocms/structured-text/blob/main/packages/slate-utils/src/types.ts#L267) and [type guards](https://github.com/datocms/structured-text/blob/main/packages/slate-utils/src/guards.ts) for the Slate format, so you know what you can expect to read and write in there.

In this example, we're building a function that removes every link present in the content:

```typescript
import { Node, isLink, isNonTextNode, NonTextNode } from 'datocms-structured-text-slate-utils';
import clone from 'clone-deep';

function visit(
  tree: Node | Node[],
  callback: (node: Node, index: number, parents: Node[]) => void,
) {
  const all = (nodes: Node[], parents: Node[]) =>
    nodes.forEach((node, index) => one(node, index, parents));

  const one = (node: Node, index: number, parents: Node[]) => {
    if ('children' in node) {
      all(node.children, [node, ...parents]);
    }
    callback(node, index, parents);
  };

  if (Array.isArray(tree)) {
    all(tree, []);
  } else {
    one(tree, 0, []);
  }
}

function removeLinks(slateValue: Node[]) {
  const value = clone(slateValue);

  visit(value, (node, index, parents) => {
    if (!isNonTextNode(node) || !isLink(node)) {
      return;
    }

    const parent = parents[0] as NonTextNode;
    parent.children.splice(index, 1, ...node.children);
  });

  return value;
}

ctx.setFieldValue(
  'my_structured_text_field',
  removeLinks(ctx.formValues['my_structured_text_field'] as Node[]),
);
```

Structured text fields can contain both references to other records via its [`itemLink`](/docs/structured-text/dast.md#itemLink) and [`inlineItem`](/docs/structured-text/dast.md#inlineItem) nodes, and blocks via its [`block`](/docs/structured-text/dast.md#block) nodes. The Slate representation for them is similar to the following:

```json
[
  {
    "type": "paragraph",
    "children": [
      {
        "text": "This is a "
      },
      {
        "type": "itemLink",
        "item": "78722383",
        "itemTypeId": "810907",
        "children": [
          {
            "text": "link to a record"
          }
        ]
      },
      {
        "text": " and this is an inline record: "
      },
      {
        "type": "inlineItem",
        "item": "69045807",
        "itemTypeId": "810907",
        "children": [{ "text": "" }]
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "This is a block:"
      }
    ]
  },
  {
    "type": "block",
    "id": "87031498",
    "blockModelId": "810933",
    "children": [{ "text": "" }],
    "title": "Foobar"
  }
]
```

As you can see:

-   Both `itemLink` and `inlineItem` nodes have `item` and `itemTypeId` attributes that point to the referenced record;
-   Both `inlineItem` and `block` nodes need to have a `children` attribute always containing an empty span;
    
-   Blocks have the `blockModelId` attribute containing to the ID of the block model and the `id` attribute with the ID of the block, while all the other attributes depend on the actual fields of the block model itself.
    

You can create/remove/change these nodes like any other one by keeping their formats correct. In this example, we're adding a new block node at the end of the content:

```typescript
ctx.setFieldValue(
  'my_structured_text_field',
  [
    ...ctx.formValues['my_structured_text_field'],
    {
      type: 'block',
      key: `${new Date().getTime()}`,
      blockModelId: '810933',
      title: 'Foobar',
      children: [{ text: '' }],
    },
  ]
);
```

Pay attention to the `key` attribute here: to create new block nodes, you need to fill it with an unique string. When the record will be eventually saved, a new ID will be generated by the DatoCMS API, the `id` attribute will appear in the node, and the `key` attribute will be removed.

> [!WARNING] Avoid creating Editor field extensions for Structured Text fields!
> While it's perfectly fine to develop Addon field extensions for Structured Text fields, overriding the regular editor DatoCMS offers for this field type is generally not a good idea, as it requires a lot of effort to re-create a convincing editing experience.

---

# Plugin SDK — CORS issues for DatoCMS Plugins

Source [docs]: https://www.datocms.com/docs/plugin-sdk/cors-issues-for-datocms-plugins.md

## Introduction: What is CORS?

> [!PROTIP] Pro tip: Recommended pre-reading
> If you're not already familiar with Cross-Origin Resource Sharing (CORS), we recommend reading the following explainers first:
> 
> -   [**MDN: Cross-Origin Resource Sharing**](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) for a basic technical background
>     
> -   ["How to win at CORS" by Jake Archibald](https://jakearchibald.com/2021/cors/) for a longer history of CORS and why it exists

In brief, CORS is a browser-side security mechanism that lets a domain declare which *other* domains are allowed to make clientside API calls to itself. For example, `my-cors-protected-domain.com` can tell browsers that only `official-partner-site.com` can make clientside API calls to it, while rejecting all other attempts.

In context for you as a plugin author, this means:

-   Your plugin would normally be hosted on `plugins-cdn.datocms.com` (if you [published it to the DatoCMS Marketplace](/docs/plugin-sdk/publishing-to-marketplace.md))
-   If you try to make a clientside browser `fetch()` to `another-domain.com`, the other domain gets to decide whether this is allowed. If they say no (or don't return a CORS header at all), the browser will fail your request with a CORS error
    

## As a plugin author, when do I need to worry about CORS?

In the context of plugin authoring, **CORS is only an issue when all the following criteria are met:**

-   Your plugin **lives on one domain** (like our default `plugins-cdn.datocms.com`) but needs to **make an API call to another domain**, like a third-party translation provider, an LLM API, etc.
-   The other **API endpoint does not natively support CORS**, or does not allow our originating domain (`plugins-cdn.datocms.com`) in its CORS setup.
    
-   The API call is made via a **clientside JavaScript** **`fetch()`** **or** **`XMLHttpRequest`**
    

Only when ALL of these conditions are true will you need to work around CORS.

### When CORS is NOT an issue

You do NOT have to worry about CORS if your plugin:

-   Is only making calls to DatoCMS endpoints (like the [Content Management API](/docs/content-management-api.md) endpoints). We handle CORS for you when a plugin makes calls to other Dato-owned endpoints.
-   Is proxying its requests *serverside* (such as via a Next API route, serverless/lambda function, or your own backend). CORS only affects cross-origin **browser** (clientside) requests, not server-to-server.
    
-   Is making requests to the same *origin* (scheme + domain + port) it's hosted on. This is most common for private, internal-use plugins, such as one hosted on `www.your-domain.com/plugin` and making API calls to `www.your-domain.com/api`. Note that different *subdomains* (like `plugin.your-domain.com`) DO count as cross-origin traffic, and ARE subject to CORS rules. Different ports have the same problem (`your-domain.com:443` is a different origin than `your-domain.com:1234`). In those cases, see below for solutions.
    

## How do I fix CORS issues with my plugin?

### Scenario 1: Add CORS headers to an API endpoint under your control

If you control the destination endpoint (i.e., it's your own backend API you want to fetch from), then the proper solution is to add the correct CORS headers to that endpoint so that it will allow `fetch()` from `plugins-cdn.datocms.com`.

If you're not sure how to add the correct CORS headers, these resources may help (or just ask your favorite LLM):

-   "[Will it CORS?](https://httptoolkit.com/will-it-cors/)", an interactive wizard that walks you through CORS troubleshooting step-by-step
-   [How to enable CORS on Vercel](https://vercel.com/kb/guide/how-to-enable-cors)
    
-   [Handling CORS on Netlify](https://answers.netlify.com/t/support-guide-handling-cors-on-netlify/107739)
-   [Setting CORS headers with Astro on Netlify](https://dev.to/cassidoo/three-ways-to-set-headers-with-netlify-and-astro-1iib)
    
-   [AWS Lambda Function URL with CORS explained by example](https://dev.to/rimutaka/aws-lambda-function-url-with-cors-explained-by-example-14df)
    

### **Scenario 2: Use a serverside CORS proxy for third-party endpoints**

(We also provide an official proxy for DatoCMS plugins! See below.)

If you do not control the destination endpoint (it's a third-party API), you will need to create a workaround, usually via a serverside proxy that injects the proper CORS headers into the third-party API's response before returning it to the browser.

You can either make your own (see [Cloudflare Workers CORS proxy example](https://developers.cloudflare.com/workers/examples/cors-header-proxy/)), or use the proxy DatoCMS provides specifically for this use, below.

Your own proxy doesn't need be on Cloudflare or serverless or any particular server. CORS is enforced only by web browsers, so any server-side environment (Node.js, a serverless function, your existing backend) can make the request on your plugin's behalf. The proxy just has to do two things: forward the request to the target API, and return the response with its own CORS headers injected. The Cloudflare Workers example is just to show you the basic concept. Any modern LLM can write a CORS proxy for you in the environment of your choice.

## DatoCMS CORS Proxy for Plugins

For plugin authors running into CORS issues, we provide an official proxy to help bypass them. This is similar in spirit to the Cloudflare Workers proxy example, above, but tweaked specifically for the needs of DatoCMS plugins.

### How to use

Usage is simple: Replace your existing `fetch()` calls to the third-party endpoint with our proxy URL, `cors-proxy.datocms.com`, and pass it the original target URL as the `url` search param, URL-encoded.

It is just an HTTP proxy. There's nothing for you to download or install, just a simple rewrite in your code.

### **Example**

```javascript
// The API you're actually trying to reach
const targetUrl = 'https://www.third-party.com/api';

// But instead of calling it directly, route it through our proxy
// The `url` param must be URL-encoded, since the target URL contains
// characters (:, /, ?) that would otherwise break the proxy URL.
const response = await fetch(
  `https://cors-proxy.datocms.com/?url=${encodeURIComponent(targetUrl)}`,
);

// Continue processing the response, e.g.
// const data = await response.json()
// doSomethingWith(data);
```

This is a simple proxy that sits between your plugin and the destination API endpoint. Its sole job is to inject the necessary CORS headers into the third-party API response before returning it to the browser, thereby fooling the browser and bypassing the CORS errors.

### Limitations

Our official proxy has some limitations you should be aware of:

-   Only Marketplace-hosted plugins on `plugins-cdn.datocms.com` and `localhost` (any port) are allowed. If your plugin is hosted internally or on another domain, our proxy will not work.
-   **Supported request headers** (that you send)**:** Only `Content-Type` and `Authorization`. Other headers will cause an error.
    
-   **Supported HTTP methods:** Only `GET`, `POST`, `PUT`, `HEAD`, `DELETE`, and `OPTIONS`. `PATCH` and other methods are not supported and will cause an error.
-   **No SLA, performance, or uptime guarantee:** The proxy is provided as-is, on a best-effort basis. We make no guarantee as to its availability or performance at any time, regardless of your plan or other agreements with us. It is not considered a part of your regular service level agreement (SLA), if you have one. That said, it's usually stable and rarely has issues.
    

If you need functionality not supported by our official proxy, you should write your own (see the Cloudflare Workers example in Scenario 2, above).

### Privacy

When you use the CORS proxy, your request will go through DatoCMS servers and our upstream providers. We may log some information for debugging purposes (such as the target URL); such logs are only used for troubleshooting and to fix issues with the CORS proxy. However, we don't take any special effort to scrub any information that may be in these logs.

We recommend only using our proxy for non-sensitive data — any trade secrets, personally identifiable information, or other sensitive information should not go through this proxy.

The destination API will see the request as coming from our IP address instead of yours.

---

# Plugin SDK — Publishing to Marketplace

Source [docs]: https://www.datocms.com/docs/plugin-sdk/publishing-to-marketplace.md

If you've created a new plugin, we strongly encourage you to share it with the community as an [NPM](https://www.npmjs.com/) package, so that it will become available in our [Marketplace](https://www.datocms.com/marketplace/plugins.md) and installable on every DatoCMS project in one click!

### Tweaking the `package.json`

To release a plugin, you need to make sure to fill the `package.json` with these information:

```json
{
  "name": "datocms-plugin-foobar",
  "version": "0.0.1",
  "homepage": "https://github.com/mark/foobar#readme",
  "description": "Add a small description for the plugin here",
  "keywords": ["datocms-plugin"],
  "datoCmsPlugin": {
    "title": "Plugin title",
    "coverImage": "docs/cover.png",
    "previewImage": "docs/preview.mp4",
    "entryPoint": "build/index.html",
    "permissions": [],
  },
  "devDependencies": { ... },
  "dependencies": { ... }
}
```

The following table describes the properties that can be set on the file:

-   `name` (required): NPM package name
-   `version` (required): Plugin version
    
-   `description` (required): Short description of what the plugin does
-   `keywords` (required): Plugin keywords, useful to help users find your plugin
    
-   `homepage`: URL of the plugin homepage, will be shown in the Marketplace
-   `datoCmsPlugin.title` (required): Plugin title
    
-   `datoCmsPlugin.entryPoint` (required): Relative path to the plugin entry point
-   `datoCmsPlugin.coverImage`: Relative path to an image in the repo (`.png`, `.webp`, `.gif`, `.jpeg`, `.jpg`) that will be used as the plugin's cover image (banner) in the [Marketplace](https://www.datocms.com/marketplace/plugins.md). SVGs arenotsupported due to CDN limitations; please rasterize them to one of the supported formats instead.
    
-   `datoCmsPlugin.previewImage`: Relative path to a file in the repo, either an image (`.png`, `.webp`, `.gif`, `.jpeg`, `.jpg`) or a short video (`.mp4`, `.mov`) showing the plugin in action. Videos demoing the real plugin flow are preferred. SVGs arenot supported.
-   `datoCmsPlugin.permissions` (required): [Additional permissions](/docs/plugin-sdk/additional-permissions.md) your plugin needs to work
    

Make sure to strictly follow these rules, otherwise the plugin won't be visible in the Marketplace:

-   `name` MUST start with `datocms-plugin-`;
-   `entryPoint`, `previewImage` and `coverImage` MUST be files contained in the package, and need to be defined as paths relative to the package root (ie. `docs/image.png`);
    
-   `keywords` MUST contain the `datocms-plugin` keyword;
    

### Publishing the plugin

It is now time to publish your plugin as an NPM package. Inside your project, run the following command:

Terminal window

```bash
npm publish
```

Once published, the plugin will automatically be added in the Marketplace within one hour. The same applies also to new version releases.

> [!WARNING] Not showing up in the Marketplace?
> If you plugin is not showing up after 3 hours then please triple check that you've followed all the rules above in your `package.json`, then contact support.

If you have previously developed a plugin that was private, you can still publish it to the marketplace in the future. In the plugin menu within the CMS, clicking on "Switch to Marketplace version" points a private plugin instance at an already-published package allowing you to publish the plugin to the marketplace. Its configured parameters and field assignments are left exactly as they are.

### Plugin upgrades

To release a new version of your plugin, follow the [specific guide](/docs/plugin-sdk/releasing-new-plugin-versions.md). Once you publish a new version, projects who have installed it will receive a notification asking them to upgrade:

(Image content)

Make sure in the new versions to [handle legacy configuration options properly](/docs/plugin-sdk/event-hooks.md)!

### A word about external JS/CSS files required by the iframe

If your plugin is called `datocms-plugin-foobar` and the entry point specified in the `package.json` is `build/index.html`, the URL that will be loaded as an iframe will be:

```plaintext
https://plugins-cdn.datocms.com/datocms-plugin-tag-editor@0.1.2/build/index.html
```

This means that if the page requires a JS file with an absolute path like `/js/bundle.js` then it won't work, as the final URL will be `https://plugins-cdn.datocms.com/js/bundle.js`, which will be non-existent.

In general, make sure that any external resource you require is expressed as a relative path to the HTML page!

---

# Plugin SDK — Releasing new plugin versions

Source [docs]: https://www.datocms.com/docs/plugin-sdk/releasing-new-plugin-versions.md

If you already have published a plugin on the DatoCMS Marketplace, here is what you need to do in order to release a new version.

## Developing a new version of a published plugin

To test a new local version of a plugin that has already been published, make sure to [create a new sandbox environment](/docs/scripting-migrations/introduction.md#creating-a-new-sandbox-environment), then enter the "Developer zone" settings, and specify a local entry point URL for the plugin.

(Video content)

This way, all the settings you already entered for the plugin and all the fields where you installed its manual field extensions will remain untouched, but you'll be able to test new code.

## Releasing a canary version

[Following the usual NPM convention](https://docs.npmjs.com/cli/v8/commands/npm-dist-tag#purpose), other users will see the upgrade notification for your plugin **only when a new package version is tagged as** **`latest`**. This means that you can release a canary version of your plugin that only you can test by publishing it to any other NPM `dist-tag`.

In this example, we'll use the `next` tag:

Terminal window

```bash
npm publish --tag next
```

Once the new version is published, open the "Developer zone" section and click on "Upgrade to canary release". A prompt will appear asking the exact canary version you want to install.

## Releasing an official new version

Once you made sure the canary release works as expected, you can publish a new version on the `latest` NPM tag:

Terminal window

```bash
npm publish
```

Once published, all the projects where the plugin is installed will receive a notification asking them to upgrade to the latest version:

(Image content)

Additionally, for existing plugins, there are other options you can follow using the "For Developers" submenu within the Plugin page in the CMS.

(Image content)

**Fork a plugin to develop against it safely.** "Duplicate for development" creates a private copy of any plugin, ready to point at your dev server. It offers to disable the original at the same time, so you don't end up with two near-identical entries wherever plugins show up while you're mid-development. The copy isn't assigned to any field, so if you need to test it where the original is in use, reassign that field to the copy by hand.

**Or skip duplicating altogether.** "Point to local server" allows you to point the existing plugin straight at your dev server without making a copy, and start making your changes on it. It detaches the plugin from npm (it becomes private, and stops receiving Marketplace updates) until you point it back with "Switch to Marketplace version".

If you want to pin any marketplace plugin you manage to a specific version, **"Switch to a different version"** is the best way. This pins any marketplace plugin version (pre-releases included). It's how you'd stage the rollout of a new release for a popular plugin: publish it to npm without tagging it `latest`, pin that exact version in the CMS to try it yourself on a real project, and if everything's right, then tag it `latest` on npm so it propagates to every other project tracking the package.

## Migrating old global plugin settings

The new version might need to store different settings than the previous ones. This can happen both for [global settings](/docs/plugin-sdk/config-screen.md), or the settings associated to a particular use of a [manual extension](/docs/plugin-sdk/manual-field-extensions.md) inside a field.

If the end-user decides to upgrade to the latest version of the plugin, DatoCMS keeps the old settings saved. This means that **plugins have to somehow manage configuration objects in older formats** too.

Let's concentrate on global plugin settings first, `ctx.plugin.attributes.parameters`. We can easily build some Typescript types and [type guard functions](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) to properly describe all the possibile formats in which settings might be stored:

```typescript
// ctx.plugin.attributes.parameters can be in one of these formats:
type Config = EmptyConfig | V1Config | V2Config;

// As soon as the plugin is installed, config is an empty object:
type EmptyConfig = {};

// Plugin v1 version saves config in this format:
type V1Config = {
  someOption: 'yes' | 'no';
}

// Current version changes the format for `someOption`, and adds `newOption`:
type V2Config = {
  someOption: boolean;
  newOption: string;
}

const isEmptyConfig = (parameters: Config): parameters is EmptyConfig => {
  return Object.keys(parameters).length === 0;
}

const isV1Config = (parameters: Config): parameters is V1Config => {
  return 'someOption' in parameters;
}

const isV2Config = (parameters: Config): parameters is V1Config => {
  return 'newOption' in parameters;
}
```

In this example, new and old config formats are somewhat compatible, so we can use the [`onBoot`](/docs/plugin-sdk/event-hooks.md) hook — which gets called as soon as the DatoCMS application loads, or the plugin is installed for the first time — to silently update the plugin configuration to the new format or, if it's the first installation for the plugin, to provide some default configuration:

```typescript
function normalizeConfig(parameters: Config): V2Config {
  if (isEmptyConfig(parameters)) {
    return { someOption: true, newOption: 'foobar' };
  }

  if (isV1Config(parameters)) {
    return { someOption: parameters.someOption === 'yes', newOption: 'foobar' };
  }

  return parameters;
}

connect({
  onBoot(ctx: OnBootCtx) {
    if (isV2Config(ctx.plugin.attributes.parameters as Config)) {
      return;
    }

    if (ctx.currentRole.meta.final_permissions.can_edit_schema) {
      ctx.updatePluginParameters(
        normalizeConfig(ctx.plugin.attributes.parameters as Config),
      );
    }
  },
  renderPage(pageId, ctx) {
    const parameters = normalizeConfig(ctx.plugin.attributes.parameters as Config);
    // ...use the normalized config from now on
  },
});
```

#### What if new config format is not compatible with older ones?

Unfortunately, it can also happen to introduce changes in a newer version that are not backward compatible. In this case, our approach will slighly change, as we need one of the project admins to manually change the settings in the config screen:

```typescript
connect({
  async onBoot(ctx: OnBootCtx) {
    if (isConfigValid(ctx.plugin.attributes.parameters as Config)) {
      return;
    }

    if (!ctx.currentRole.meta.final_permissions.can_edit_schema) {
      ctx.customToast({
        type: 'warning',
        message:
          'Invalid settings. Please ask your administrators to fix the issue!',
      });

      return;
    }

    const result = await ctx.customToast({
      type: 'warning',
      message:
        'Invalid settings. Please fix them to make the plugin work again!',
      cta: {
        label: 'Go to plugin settings',
        value: 'settings',
      },
    });

    if (result === 'settings') {
      ctx.navigateTo(`/admin/plugins/${ctx.plugin.id}/edit`);
    }
  },
  renderPage(pageId, ctx) {
    // fast return
    if (!isConfigValid(ctx.plugin.attributes.parameters as Config)) {
      return <div>Functionality disabled until settings are fixed!</div>;
    }
  },
});
```

Let's review what this code is doing:

-   every hook that needs to read the configuration object (`renderPage` in this example) can use the `isConfigValid()` function to test if it can execute normally, or it needs to fast return to avoid errors due to incompatible settings;
-   the `onBoot` hook shows a notification to the end user telling that the configuration needs to be manually fixed, or the plugin won't work.
    

## Migrating old manual Field Extension settings

A very similar approach can also be used to handle changes in manual field extension settings between versions.

If the new configuration is compatible with the old one:

-   the `renderFieldExtension` hook uses a `normalizeParameters()` function to convert older configuration objects into the latest format;
-   the `onBoot` hook first needs to determine if it needs to do the migration: to do that, it can look into `ctx.plugin.attributes.parameters` and see if ie. global settings have already some flag. Then it fetches all the fields that are hooked to our plugin using `ctx.loadFieldsUsingPlugin()`, and for each of them it uses the `ctx.updateFieldAppearance()` function to silently update the field extension to the new format.
    

```typescript
connect({
  async onBoot(ctx: OnBootCtx) {
    if (
      ctx.plugin.attributes.parameters.version !== '2' ||
      !ctx.currentRole.meta.final_permissions.can_edit_schema
    ) {
      return;
    }

    const fields = await ctx.loadFieldsUsingPlugin();

    await Promise.all(
      fields.map(async (field) => {
        const { appearance } = field.attributes;
        const changes: FieldAppearanceChange[] = [];

        if (
          appearance.editor === ctx.plugin.id &&
          appearance.field_extension === 'oldFieldEditorName'
        ) {
          changes.push({
            operation: 'updateEditor',
            newFieldExtensionId: 'newFieldEditorName',
            newFieldExtensionParameters: normalizeConfig(appearance.parameters),
          });
        }

        if (changes.length > 0) {
          await ctx.updateFieldAppearance(field.id, changes);
        }
      }),
    );
  },
  renderFieldExtension(fieldExtensionId, ctx) {
    const parameters = normalizeConfig(ctx.parameters);
    // ...use the normalized config from now on
  },
});
```

If old versions and new versions are incompatible, just like with the global settings before, all we can do is warning the user that manual field extensions need to be manually reconfigured. The Config screen can then offer some kind of UI to help users migrate manual field extensions in batch by providing some options:

```typescript
connect({
  async onBoot(ctx: OnBootCtx) {
    if (ctx.plugin.attributes.parameters.version === '2') {
      return;
    }

    if (!ctx.currentRole.meta.final_permissions.can_edit_schema) {
      ctx.customToast({
        type: 'warning',
        message:
          'Invalid settings. Please ask your administrators to fix the issue!',
      });

      return;
    }

    const result = await ctx.customToast({
      type: 'warning',
      message:
        'Invalid settings. Please fix them to make the plugin work again!',
      cta: {
        label: 'Go to plugin settings',
        value: 'settings',
      },
    });

    if (result === 'settings') {
      ctx.navigateTo(`/admin/plugins/${ctx.plugin.id}/edit`);
    }
  },
  renderFieldExtension(fieldExtensionId, ctx) {
    if (ctx.plugin.attributes.parameters.version !== '2') {
      return <div>Functionality disabled until settings are fixed!</div>;
    }

    // ...
  },
});
```

---

# Plugin SDK — Migrating from legacy plugins

Source [docs]: https://www.datocms.com/docs/plugin-sdk/migrating-from-legacy-plugins.md

A completely revamped plugin SDK was released in November 2021. Plugins leveraging the legacy SDK will continue to work indefinitely, but are much more limited in their possibilities, as they can only manage what in the new SKD are called [manual field extensions](/docs/plugin-sdk/manual-field-extensions.md). All the other extension points are not available.

> [!WARNING] Legacy SDK docs
> If you're looking for the Legacy SDK documentation, it is still available [here](/docs/legacy-plugins.md).

If you are interested in migrating to the new SDK, please note the following points:

## Global configuration parameters

Global parameters no longer need to be declared in the `package.json`, but are configurable through the [config-screen hooks](/docs/plugin-sdk/config-screen.md).

The data storage format is now also completely custom, as well as the interface that is shown to end users.

## Manual extensions

The old options:

-   "Type of plugin" (field editor, field add-on or sidebar widget), and
-   "Types of field" (specifying where it's possible to use the legacy plugin)
    

do not have to be declared in the `package.json` anymore, but are configurable through the [`manualFieldExtensions` hook](/docs/plugin-sdk/manual-field-extensions.md). The old "sidebar widget" plugin type is nothing but an additional `asSidebarPanel` option you can pass to the new `ManualFieldExtension` type:

```typescript
import { connect, Field, InitCtx } from 'datocms-plugins-sdk';

connect({
  manualFieldExtensions(ctx: InitCtx) {
    return [
      {
        id: 'sidebarWidget',
        name: 'My sidebar widget',
        type: 'editor',
        fieldTypes: ['integer'],
        asSidebarPanel: true,
      },
    ];
  },
  renderFieldExtension(id, ctx) {
    // ...
  },
});
```

## Instance configuration options

Instance parameters no longer need to be declared in the `package.json`, but are now completely arbitrary, as well as the interface that is shown to end users. Take a look at this part of the [documentation](/docs/plugin-sdk/manual-field-extensions.md#add-per-field-configuration-options-to-manual-field-extensions).

## `plugin.xxx` methods and properties

All methods and information previously available through `plugin.xxx` calls is now available through the `ctx` argument of the [`renderFieldExtension` hook](/docs/plugin-sdk/field-extensions.md#rendering-the-field-extension).

## Migrating appearance on associated fields

Since new plugins can expose multiple manual field extensions, you need to implement an [`onBoot` hook](/docs/plugin-sdk/event-hooks.md) to properly set the `fieldExtensionId` attribute on each field that was previously hooked with the plugin.

##### Example to migrate old field editors or sidebar widgets

```tsx
connect({
  // plugin exposes a `myExtension` manual field extension
  manualFieldExtensions(ctx: InitCtx) {
    return [
      {
        id: 'myExtension',
        name: 'Foo bar',
        type: 'editor',
        fieldTypes: ['integer'],
      },
    ];
  },
  async onBoot(ctx: OnBootCtx) {
    // if we already performed the migration, skip
    if (ctx.plugin.attributes.parameters.migratedFromLegacyPlugin) {
      return;
    }

    // if the current user cannot edit fields' settings, skip
    if (!ctx.currentRole.meta.final_permissions.can_edit_schema) {
      return;
    }

    // get all the fields currently associated to the plugin...
    const fields = await ctx.loadFieldsUsingPlugin();

    // ... and for each of them...
    await Promise.all(
      fields.map(async (field) => {
        // set the fieldExtensionId to be the new one
        await ctx.updateFieldAppearance(field.id, [{
          operation: 'updateEditor',
          newFieldExtensionId: 'myExtension',
        }]);
      }),
    );

    // save in configuration the fact that we already performed the migration
    ctx.updatePluginParameters({
      ...ctx.plugin.attributes.parameters,
      migratedFromLegacyPlugin: true,
    });
  },
});
```

##### Example to migrate old field addons

```tsx
connect({
  // plugin exposes a `myExtension` manual field extension
  manualFieldExtensions(ctx: InitCtx) {
    return [
      {
        id: 'myExtension',
        name: 'Foo bar',
        type: 'addon',
        fieldTypes: ['integer'],
      },
    ];
  },
  async onBoot(ctx: OnBootCtx) {
    // if we already performed the migration, skip
    if (ctx.plugin.attributes.parameters.migratedFromLegacyPlugin) {
      return;
    }

    // if the current user cannot edit fields' settings, skip
    if (!ctx.currentRole.meta.final_permissions.can_edit_schema) {
      return;
    }

    // get all the fields currently associated to the plugin...
    const fields = await ctx.loadFieldsUsingPlugin();

    // ... and for each of them...
    await Promise.all(
      fields.map(async (field) => {
        const { appearance } = field.attributes;
        const changes: FieldAppearanceChange[] = [];

        // find where our plugin is used...
        appearance.addons.forEach((addon, index) => {
          // set the fieldExtensionId to be the new one
          changes.push({
            operation: 'updateAddon',
            index,
            newFieldExtensionId: 'myExtension',
          });
        });

        await ctx.updateFieldAppearance(field.id, changes);
      }),
    );

    // save in configuration the fact that we already performed the migration
    ctx.updatePluginParameters({
      ...ctx.plugin.attributes.parameters,
      migratedFromLegacyPlugin: true,
    });
  },
});
```

---

# Plugin SDK — Upgrading plugins for dark mode

Source [docs]: https://www.datocms.com/docs/plugin-sdk/upgrading-plugins-for-dark-mode.md

This guide covers upgrading a plugin from `datocms-react-ui` / `datocms-plugin-sdk` **v2.1.5** (the last release without dark-mode support) to the new version that introduces semantic color tokens and full theme-aware rendering.

## What changes

###### New semantic color tokens

`Canvas` has always injected a small set of CSS variables onto its wrapper div (`--accent-color`, `--primary-color`, `--light-color`, `--dark-color`). Newer versions expand that to [full semantic palette](/docs/plugin-sdk/react-datocms-ui.md#colors). The host computes every token for the active theme and sends them via the new `ctx.cssDesignTokens` field; `Canvas` applies them verbatim.

All built-in components (`Button`, `TextInput`, `Section`, `Dropdown`, `Toolbar`, …) have been updated to use these tokens and now adapt to the active theme automatically.

###### New `ctx.colorScheme`

The new `ctx.colorScheme` property is either `'light'` or `'dark'`. For non-CSS decisions (image sources, syntax-highlighting presets, third-party widget themes) branch on `ctx.colorScheme` directly:

```jsx
<img src={ctx.colorScheme === 'dark' ? logoDark : logoLight} />
```

The SDK also reflects this onto `document.documentElement` in two ways:

-   **`data-color-scheme`** **attribute**: use it in CSS selectors like `[data-color-scheme="dark"] .my-panel`
-   **`color-scheme`** **CSS property**: enables `light-dark()` and makes native form controls/scrollbars match.
    

###### Deprecated: `ctx.theme` and the legacy CSS variables

`ctx.theme` is still present and its shape is unchanged, but the host now **pins it to light-mode values only**, regardless of what theme the user has selected. The CSS variables derived from it (`--accent-color`, `--primary-color`, `--light-color`, `--dark-color`, `--semi-transparent-accent-color`) likewise always emit light values. The older structural CSS variables emitted by `Canvas` (`--base-body-color`, `--border-color`, `--alert-color`, etc.) are also deprecated.

## Upgrade steps

#### Option 1: Let an AI agent do it

The migration is entirely mechanical: bump the dependency, swap CSS variable names, replace hardcoded colors with semantic tokens. There are no judgment calls that require human review.

Copy the prompt below and paste it into Codex, Claude Code, or any other AI coding agent, and it will handle the full upgrade and verify the result in dark mode.

    Your task is to upgrade this DatoCMS plugin from legacy CSS variables to the new semantic color token system, then verify it looks correct in dark mode.
    
    **Before starting**, create a task list covering every step below. Mark each task done as soon as it's completed. When you reach step 5 ("Find what the plugin renders"), discover the hooks first, then add one task per UI surface to the same list before proceeding.
    
    ## 1. Upgrade dependencies
    
    ```bash
    npm install datocms-react-ui@latest datocms-plugin-sdk@latest
    ```
    
    ## 2. Find all files that need updating
    
    Search for legacy CSS variables across all source files:
    
    ```bash
    grep -rn --include="*.css" --include="*.tsx" --include="*.ts" --include="*.jsx" --include="*.js" \
      -E "(--accent-color|--primary-color|--light-color|--dark-color|--semi-transparent-accent-color|--base-body-color|--light-body-color|--placeholder-body-color|--light-bg-color|--lighter-bg-color|--disabled-bg-color|--border-color|--darker-border-color|--alert-color|--warning-color|--notice-color|--warning-bg-color|--add-color|--remove-color)" \
      src/
    ```
    
    Also search for hardcoded color values:
    
    ```bash
    grep -rn --include="*.css" --include="*.tsx" --include="*.ts" --include="*.jsx" --include="*.js" \
      -E "(#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\()" \
      src/
    ```
    
    ## 3. Replace legacy CSS variables
    
    For each occurrence, look at the context (what the rule does, what element it styles) and pick the **most semantically correct** token from the full vocabulary below. The mapping table is a starting point for common cases, but the token list is the authoritative reference — always prefer the token whose description best matches the actual usage.
    
    **Mapping table (common cases):**
    
    | Legacy variable | Likely replacement |
    |---|---|
    | `--base-body-color` | `--color--ink` |
    | `--light-body-color` | `--color--ink-subtle` |
    | `--placeholder-body-color` | `--color--ink-placeholder` |
    | `--light-bg-color` | `--color--surface-muted` |
    | `--lighter-bg-color` | `--color--surface-muted` |
    | `--disabled-bg-color` | `--color--disabled--surface` |
    | `--border-color` | `--color--border` |
    | `--darker-border-color` | `--color--border-hover` |
    | `--alert-color` (as text/border) | `--color--danger-soft--ink` |
    | `--alert-color` (as background) | `--color--danger-soft--surface` |
    | `--warning-color` | `--color--warning-soft--ink` |
    | `--warning-bg-color` | `--color--warning-soft--surface` |
    | `--notice-color` | `--color--success-soft--ink` |
    | `--add-color` (as background) | `--color--diff-added--surface` |
    | `--remove-color` (as background) | `--color--diff-removed--surface` |
    | `--accent-color` (as text/link) | `--color--ink-link` |
    | `--accent-color` (as background) | `--color--primary--surface-secondary` |
    | `--accent-color` (as hover border) | `--color--focus--border` |
    | `--semi-transparent-accent-color` | `--color--focus--outline` |
    | `--primary-color` | `--color--primary--surface` |
    | `--light-color` | `--color--primary-soft--surface` |
    | `--dark-color` (as background) | `--color--primary--surface-secondary` |
    | `--dark-color` (as text) | `--color--primary--ink` |
    
    **Full token vocabulary:**
    
    ```
    Standalone (neutral page)
    --color--surface                    Page background
    --color--surface-hover              Hovered row in lists/tables
    --color--surface-muted              Muted section panels, quiet cards
    --color--surface-raised             Elevated layer: modals, dropdowns, popovers
    --color--surface-raised-hover       Hovered option inside a dropdown
    --color--surface-raised-active      Focused/pressed option inside a dropdown
    --color--ink                        Primary body text
    --color--ink-subtle                 Secondary text, captions, helper labels
    --color--ink-hover                  Toolbar icon/link fill on hover
    --color--ink-muted                  Deemphasized text
    --color--ink-placeholder            Empty-input placeholder text
    --color--ink-primary                Theme-colored text/icons for branded labels
    --color--ink-link                   Inline links and accent text
    --color--ink-danger                 Error text/icon on a neutral surface
    --color--ink-warning                Warning text/icon on a neutral surface
    --color--ink-success                Success text/icon on a neutral surface
    --color--ink-disabled               Label color on disabled inputs/buttons
    --color--border                     Default 1px divider
    --color--border-hover               Border of an input/card when hovered
    
    Primary (brand color, full strength)
    --color--primary--surface           Resting background of a primary CTA button
    --color--primary--surface-hover     Hovered primary button
    --color--primary--surface-active    Pressed primary button
    --color--primary--surface-muted     Muted variant of the primary surface
    --color--primary--surface-secondary Quieter brand surface for accent badges/chips
    --color--primary--ink               Text/icon on any primary surface
    --color--primary--border            Border on top of a primary surface
    
    Primary-soft (tinted, secondary actions)
    --color--primary-soft--surface      Resting background of secondary brand-tinted buttons
    --color--primary-soft--surface-hover
    --color--primary-soft--surface-active
    --color--primary-soft--ink          Text/icon on a soft brand surface
    --color--primary-soft--border
    
    Selected (active entry in a list/tree/gallery)
    --color--selected--surface
    --color--selected--surface-hover
    --color--selected--ink
    --color--selected--border
    
    Disabled
    --color--disabled--surface
    --color--disabled--ink
    
    Danger (destructive actions)
    --color--danger--surface
    --color--danger--ink
    
    Focus rings
    --color--focus--border              Border color of the focused element
    --color--focus--outline             Soft outline ring around focused element
    
    Signal tones (validation states, notifications)
    --color--danger-soft--surface       Error banner/alert background
    --color--danger-soft--ink           Error message text/icon
    --color--danger-soft--border        Border around invalid input/alert
    --color--danger-soft--outline       Soft halo around invalid field on focus
    --color--warning-soft--surface
    --color--warning-soft--ink
    --color--warning-soft--border
    --color--warning-soft--outline
    --color--success-soft--surface
    --color--success-soft--ink
    --color--success-soft--border
    --color--success-soft--outline
    
    Diffs
    --color--diff-added--surface        Background of inline added text
    --color--diff-added--outline        Outline around a block-level added panel
    --color--diff-added--ink            Left-border color (vivid, recently changed)
    --color--diff-added--ink-subtle     Left-border color (stable)
    --color--diff-removed--surface
    --color--diff-removed--outline
    --color--diff-removed--ink
    --color--diff-removed--ink-subtle
    --color--diff-changed--surface
    --color--diff-changed--outline
    
    Backdrop / overlay
    --color--backdrop--surface          Full-screen modal dim
    --color--backdrop--ink              Icon color for close controls on backdrop
    --color--overlay--surface           Scrim over media thumbnails
    --color--overlay--surface-hover
    --color--overlay--surface-active
    --color--overlay--ink
    
    Stacked (dark inline panels, asset uploaders, players)
    --color--stacked--surface
    --color--stacked--surface-upper
    --color--stacked--surface-action
    --color--stacked--surface-action-hover
    --color--stacked--surface-action-active
    --color--stacked--ink
    --color--stacked--ink-subtle
    --color--stacked--border
    
    Other
    --color--highlight--surface         Yellow marker in rich-text editors
    --color--progress--track
    --color--progress--fill
    --color--progress--fill-hover
    --color--tooltip--surface
    --color--tooltip--surface-hover
    --color--tooltip--ink
    --color--tooltip--ink-subtle
    --color--code--surface
    --color--code--ink
    --color--scrollbar--fill
    --color--status-draft--ink
    --color--status-outdated--ink
    --color--status-published--ink
    ```
    
    **Rule: never cross ink-owning contexts.**
    
    Each context (`primary`, `primary-soft`, `danger`, `danger-soft`, `warning-soft`, `success-soft`, `disabled`, `selected`, `stacked`, `overlay`, `tooltip`, `code`…) is contrast-balanced as a unit. Always pair a surface with the ink from the same context — e.g. `--color--danger-soft--surface` + `--color--danger-soft--ink`. Mixing across contexts (e.g. `--color--primary--ink` on `--color--danger-soft--surface`) produces illegible combinations in dark mode.
    
    ## 4. Replace hardcoded colors
    
    For each hardcoded color, pick the most semantically correct token from the vocabulary above. Common patterns:
    
    - Muted text (`#999`, `#aaa`, grey tones) → `--color--ink-subtle`
    - Body text (`#333`, `#444`, dark tones) → `--color--ink`
    - White backgrounds → `--color--surface`
    - Light grey backgrounds → `--color--surface-muted`
    - Border greys → `--color--border`
    
    For colors that are genuinely custom (brand illustrations, data-viz, third-party widgets), define them with a dark-mode override:
    
    ```css
    .my-element {
      --my-custom: #4a90e2;
    }
    
    [data-color-scheme="dark"] .my-element {
      --my-custom: #6aa9ec;
    }
    ```
    
    ## 5. Check inline styles in TSX/JSX
    
    ```bash
    grep -rn --include="*.tsx" --include="*.jsx" -E "style=.*(color|background|border)" src/
    ```
    
    Apply the same token substitutions. If the color is set dynamically from `ctx.theme`, migrate to `ctx.cssDesignTokens` or use CSS variables instead.
    
    ## 6. Check SVG fills
    
    ```bash
    grep -rn --include="*.tsx" --include="*.jsx" --include="*.svg" -E 'fill="(?!currentColor|none)' src/
    ```
    
    Replace hardcoded fills with `fill="currentColor"` so icons inherit the surrounding text color.
    
    ## 7. Test in dark mode
    
    Before starting the browser steps, load the agent-browser command reference:
    
    ```bash
    agent-browser skills get core             # workflows, common patterns, troubleshooting
    agent-browser skills get core --full      # full command reference and templates
    ```
    
    **Browser interaction pattern:** always use `snapshot -i` to get element refs, then interact via refs — never use `eval` or raw CSS selectors, which are fragile:
    
    ```bash
    agent-browser snapshot -i                  # list interactive elements with @refs
    agent-browser fill @e3 "http://..."        # fill a field by ref
    agent-browser find role button click --name "Save plugin settings"
    agent-browser navigate "https://..."       # navigate within an existing session (not `open`)
    ```
    
    **When to screenshot:** only during visual inspection (steps 7f–7g below), when you need to verify colors and contrast. For everything else — confirming navigation, form state, install success — use `agent-browser get url` or `snapshot -i`. Screenshots are slow and consume extra tokens for image parsing.
    
    **Interacting with plugin iframes:** DatoCMS renders each plugin inside an iframe. Since agent-browser **0.27.0**, `snapshot -i` inlines the iframe content directly in the tree, so plugin elements appear as normal refs and you can click/fill them without any frame-switching:
    
    ```bash
    agent-browser snapshot -i
    # Output includes iframe content inline, e.g.:
    # - Iframe [ref=e25]
    #   - button "Insert new table" [ref=e28]
    
    agent-browser click @e28   # works directly — no frame switching needed
    ```
    
    For older versions (< 0.27.0), upgrade first:
    
    ```bash
    npm update -g agent-browser
    ```
    
    If you need to explicitly enter a cross-origin iframe (e.g. for debugging), use:
    
    ```bash
    agent-browser frame @e25   # switch context into the iframe
    agent-browser frame main   # return to the top-level page
    ```
    
    On cross-origin iframes, `snapshot` inside the frame may fail with a CDP accessibility error depending on the version. The inline-ref approach via `snapshot -i` on the parent is more reliable.
    
    ### 7a. Start the dev server
    
    ```bash
    npm run dev
    ```
    
    **Check the terminal output for the actual port** — if 5173 or 5174 are already in use, Vite picks the next available one. Use the URL printed, not the default.
    
    ### 7b. Get the bearer token
    
    Ask the user to open the DatoCMS app in their browser, open the DevTools console, and run:
    
    ```js
    JSON.parse(localStorage.getItem('persistedState')).session.bearerToken
    ```
    
    ### 7c. Open the browser in dark mode
    
    Pass `--color-scheme dark` when first opening the browser session. Add `--headed` so the user can see the browser in real time. If the daemon is already running, close it first:
    
    ```bash
    agent-browser close
    agent-browser open "https://<project>.admin.datocms.com/enter?access_token=<TOKEN>" --color-scheme dark --headed
    agent-browser wait --load networkidle
    ```
    
    This sets the OS-level `prefers-color-scheme` media query to `dark`, which DatoCMS picks up automatically.
    
    For all subsequent navigation in the same session use `agent-browser navigate <url>`, not `open`.
    
    ### 7d. Install the plugin in the test project
    
    Use the bearer token from step 7b and the dev server URL from step 7a:
    
    ```bash
    npx datocms cma:call plugins create \
      --api-token=<BEARER_TOKEN> \
      --data='{name: "Test plugin", url: "http://localhost:5175/"}'
    ```
    
    Replace the URL with the actual port printed by the dev server.
    
    ### 7e. Find what the plugin renders
    
    Grep `src/main.tsx` (or equivalent entrypoint) for the hooks the plugin registers:
    
    ```bash
    grep -rE "overrideFieldExtensions|renderFieldExtension|renderPage|renderModal|renderAssetSource|renderItemFormSidebar|renderItemFormOutlet|renderManualFieldExtensionConfigScreen|customMarksForStructuredTextField|manualFieldExtensions" src/
    ```
    
    Then set up the corresponding context in DatoCMS.
    
    **Creating test models and fields via CLI (faster than the UI):**
    
    Use the bearer token from step 7b — no prior `datocms login` or `datocms link` needed. Use `npx datocms cma:docs <resource> <action>` to look up the exact request body shape for any CMA call (e.g. `npx datocms cma:docs fields create`).
    
    ```bash
    # Create a test model
    npx datocms cma:call itemTypes create \
      --api-token=<BEARER_TOKEN> \
      --data='{name: "Test", api_key: "test_dark_mode", draft_mode_active: true}'
    
    # Create a field on it (replace <MODEL_ID> with the id returned above)
    npx datocms cma:call fields create <MODEL_ID> \
      --api-token=<BEARER_TOKEN> \
      --data='{label: "Value", api_key: "value", field_type: "string"}'
    ```
    
    Replace `field_type` with whichever type the plugin targets (e.g. `json`, `text`, `structured_text`).
    
    For each hook the plugin registers, set up the corresponding context and add one task to the list per distinct UI surface. Here's what each hook requires:
    
    - **`overrideFieldExtensions` / `renderFieldExtension`**: the plugin auto-attaches to certain field types. Create a model with a field of that type, open a record, and the plugin renders as the field editor.
    - **`manualFieldExtensions` / `renderFieldExtension`**: the plugin must be manually assigned. Create a model with a field of the declared `fieldTypes`, then assign the plugin as the field editor via CLI (use the plugin ID returned during installation and the extension id declared in `manualFieldExtensions()`):
      ```bash
      npx datocms cma:call fields update <FIELD_ID> \
        --api-token=<BEARER_TOKEN> \
        --data='{appearance: {editor: "<PLUGIN_ID>", field_extension: "<EXTENSION_ID>", parameters: {}, addons: []}}'
      ```
      Then open a record of that model.
    - **`renderManualFieldExtensionConfigScreen`**: assign the plugin to the field via CLI (same `fields update` command as above). The config screen renders inside the field settings panel in the DatoCMS UI — navigate there in the browser to inspect it:
      ```bash
      agent-browser navigate "https://<project>.admin.datocms.com/schema/item_types/<MODEL_ID>/fields/<FIELD_ID>/edit?tab=presentation"
      agent-browser wait --load networkidle
      ```
    - **`renderPage`**: the plugin adds a custom page. Find it in the navigation.
    - **`renderModal`**: triggered programmatically — find the UI element that opens it (e.g. a button in a field extension) and click it.
    - **`renderAssetSource`**: appears in the media picker. Open any asset/gallery field and look for a custom source tab.
    - **`renderItemFormSidebar` / `renderItemFormOutlet`**: go to model settings, assign the plugin to the sidebar or outlet, then open any record of that model.
    
    ### 7f. Visually inspect in dark mode
    
    Work through each UI surface task one at a time. For each, verify:
    
    - Text is legible (no dark-on-dark or light-on-light combination)
    - Borders are visible
    - Backgrounds have appropriate contrast
    - Hover/focus states are visible
    - Any modals or dropdowns opened by the plugin render correctly
    
    Take a screenshot for reference:
    
    ```bash
    agent-browser screenshot
    ```
    
    ## 8. Verify (static)
    
    Re-run the search commands from steps 2 and 3 — both should return no results. Legitimate non-color hits to ignore: `color: inherit`, `background-color: transparent`, `color: currentColor`.
    
    Then confirm the build passes:
    
    ```bash
    npm run build
    ```

#### **Option 2: Do it manually**

Update your dependencies:

Terminal window

```bash
npm install datocms-react-ui@latest datocms-plugin-sdk@latest
```

Then open the DatoCMS app in dark mode and open your plugin — it will now render with dark colors. Things to audit:

-   **Hardcoded colors in your CSS** — `color: #333`, `background: white`, etc. They won't follow the theme and contrast will break.
-   **Hardcoded SVG fills** in custom icons — switch to `fill="currentColor"` so they inherit the surrounding text color.
    
-   **Custom CSS mixed with library components** — verify the combinations look right in both light and dark.
-   **Inlined** **`style`** **props** with color values — treat the same as hardcoded CSS.3. Replace deprecated CSS variables (optional now, required later)
    

###### Migration

1.  If your plugin reads `ctx.theme` directly to build colors or styles, migrate to `ctx.cssDesignTokens`.
    
2.  Replace the legacy CSS variables with [the new semantic equivalents](/docs/plugin-sdk/react-datocms-ui.md#colors) from the table below.
    

| Legacy CSS variable | Replace with |
| --- | --- |
| \--base-body-color | \--color--ink |
| \--light-body-color | \--color--ink-subtle |
| \--placeholder-body-color | \--color--ink-placeholder |
| \--light-bg-color | \--color--surface-muted |
| \--lighter-bg-color | \--color--surface-muted |
| \--disabled-bg-color | \--color--disabled--surface |
| \--border-color | \--color--border |
| \--darker-border-color | \--color--border-hover |
| \--alert-color (as text) | \--color--danger-soft--ink |
| \--alert-color (as background) | \--color--danger-soft--surface |
| \--warning-color | \--color--warning-soft--ink |
| \--notice-color | \--color--success-soft--ink |
| \--warning-bg-color | \--color--warning-soft--surface |
| \--add-color (as background) | \--color--diff-added--surface |
| \--remove-color (as background) | \--color--diff-removed--surface |
| \--accent-color (as background) | \--color--primary--surface-secondary |
| \--accent-color (as text) | \--color--ink-link |
| \--primary-color | \--color--primary--surface |
| \--light-color | \--color--primary-soft--surface |
| \--dark-color | \--color--primary--surface (or --color--primary--ink if used as text) |
| \--semi-transparent-accent-color | \--color--focus--outline (focus rings) |

> [!WARNING] A few edge cases
> `--alert-color`, `--add-color`, and `--remove-color` were used as both foreground and background in the wild. Pick the semantic token that matches your actual usage.
> 
> **Migrate the accent group even if you only support light mode today.** `--accent-color`, `--primary-color`, `--light-color`, `--dark-color`, and `--semi-transparent-accent-color` are derived from `ctx.theme`, which is now pinned to light values only. They will be removed in a future major.

---

# DatoCMS Site Search — Site Search Overview

Source [docs]: https://www.datocms.com/docs/site-search.md

DatoCMS Site Search is a way to **deliver tailored search results to your website visitors**. You can think of it as a replacement for the now discontinued Google Site Search.

(Image content)

There are many third-party services out there that fill this need (like [SwiftType](https://swiftype.com/), [Algolia](https://www.algolia.com/), and [Cludo](https://www.cludo.com/)). Our solution seeks to be a great option for plenty of websites:

-   Extremely easy to integrate with your static website
-   Completely customizable in terms of look & feel
    
-   Minimal configuration needed
-   Handles multilingual websites nicely
    
-   included in the price of DatoCMS with no additional charges
    

#### How it works

-   Every time your website finishes being deployed, **we'll crawl it to fetch updated content.**
-   From your frontend, you can [**make AJAX requests to our Content Management API**](/docs/site-search/base-integration.md#performing-searches) **to present relevant results to your visitors**. We also provide [**React**](/docs/site-search/widget.md) **and** [**Vue**](/docs/site-search/vue-search-widget.md) **search widgets** that simplify the process.
    

> [!PROTIP] Pro tip: Integrating Algolia and DatoCMS
> If you prefer to integrate a search provider like Algolia, [this guide](https://www.datocms.com/blog/algolia-nextjs-how-to-add-algolia-instantsearch.md) demonstrates setting up a Next.js project, configuring Algolia, and creating custom search components. While the guide focuses on Algolia Intellisearch, the process for setting up other third-party services like Meilisearch, Typesense, or ElasticSearch should be relatively similar.

#### Enabling Site Search for a project

To get started, please see [Configuring DatoCMS Site Search](/docs/site-search/configuration.md).

---

# DatoCMS Site Search — Configuration

Source [docs]: https://www.datocms.com/docs/site-search/configuration.md

### A bit of context about Search Indexes

The way you configure Site Search involves the concept of search index. Search indexes tell DatoCMS to index some website: by specifying a starting URL and some other intuitive parameters, it's possible to quickly index your website and provide a tailored search experience to your website visitors.

Since the content of a DatoCMS project can be read and used on multiple frontends, multiple search indexes can be created in a single project.

Once a search index is configured, it is possible to:

1.  **Command the spidering of a website** directly from the DatoCMS interface;
    
2.  **Link one or more search index to any build trigger**, so that each time the frontend is rebuilt, the crawling of the site and re-indexing of its pages starts;
    

The configuration of the search index is as easy as possible: you specify a starting URL, and you're usually good to go. You can also specify a custom user-agent suffix for the crawler; by configuring the robots.txt file and your sitemaps properly, you can even generate completely independent search indexes for different parts of your website.

### Creating a Search Index

(Image content)

-   Go to the *Project Settings \> Search indexes* section of your project;
-   Click on the **Add a new Search index** button
    
-   Give the index a **name** and specify your **Starting URL**: that's the address from which crawling will begin;
-   Press the **Save settings** button.
    
-   You can also **link the search index to one or more build triggers**: in that case, the crawling will start every time the deployment completes successfully.
    

> [!POSITIVE] Respider a website without triggering a rebuild
> Anytime you want, you can also trigger a respidering of your website directly from CMS or [using a specific CMA endpoint](/docs/content-management-api/resources/build-trigger/reindex.md).

### Inspecting crawling results

Once the crawling of the website ends, in the *Project Settings \> Search index activity* section, you'll see a **Site spidering completed with success** event in your log.

Clicking on the **Show details** link will present you the complete list of spidered pages.

---

# DatoCMS Site Search — How the crawling works

Source [docs]: https://www.datocms.com/docs/site-search/how-the-crawling-works.md

The DatoCMS Site Search crawler is an in-house spider we created, not a standard library. We will try to explain its behaviours here.

The crawling process starts from the URL you configure as the "Starting URL" in your build trigger settings. From there, it will recursively follow all the hyperlinks pointing to your domain. It will also look for URLs you provide in sitemaps (see below).

### User Agent

The User-Agent used by our crawler is `DatoCmsSearchBot`.

### How can I control what pages will be crawled on my site?

DatoCmsSearchBot respects the [robots.txt](https://developers.google.com/search/docs/crawling-indexing/robots/create-robots-txt) directives `user-agent`, `allow`, and `disallow` (case-insensitive). We also support a simple `*` wildcard and the `$` end-of-path indicator (ignoring possible query strings).

In the example below, DatoCmsSearchBot won't crawl documents that are under `/do-not-crawl/` or `/not-allowed/` or that end in `.json`.

```plaintext
User-agent: DatoCmsSearchBot      # DatoCMS's user agent
Disallow: /do-not-crawl/          # disallow this directory
Disallow: /*.json$                # disallow all JSON files

User-agent: *                     # any robot
Disallow: /not-allowed/           # disallow this directory
```

DatoCmsSearchBot does not currently support the `crawl-delay` directive in robots.txt and robots meta tags on HTML pages such as `nofollow` and `noindex`.

At the moment we do not support robots.txt with multiple groups for the same user agent.

#### Allow and Disallow: Order matters!

If your `robots.txt` requires more complex rules, DatoCmsSearchBot only respects **the first matching** **`Allow`** **or** **`Disallow`** **directive for any given URL.** For example, with a `robots.txt` like:

```plaintext


# INCORRECT robots.txt example that accidentally disallows /other/

User-agent: DatoCmsSearchBot
Allow: /blog/
Disallow: /
Allow: /other/
```

-   `/blog/my-article`, `/blog/2/`, etc. will be crawled
-   `/other/my-page` will NOT be crawled, even though it's `Allow`ed, **because it would've first matched the** `**Disallow: /**` **line right above it**
    
-   **In other words, ALL paths not starting with** **`/blog`** **will be skipped** because of the `Disallow: /` rule. This is counterintuitive because it is the **order** of the directives, not their specificity, that our crawler respects.
    

If you wish to allow both `/blog/` and `/other/`, then you MUST place their `Allow` directives first, like:

```plaintext


# Correct robots.txt example that allows both /blog/ and /other/

User-agent: DatoCmsSearchBot
Allow: /blog/
Allow: /other/
Disallow: /
```

-   This way, everything under `/blog/` and `/other/` will be crawled
-   Everything else will be skipped
    

### Sitemaps

In addition to following the links within pages, if your website provides a Sitemap file, the crawler will use it as an additional source of URLs to crawl. [Sitemap Index files](https://developers.google.com/search/docs/advanced/sitemaps/large-sitemaps) are also supported.

The crawler will first look for [`sitemap` directives](https://developers.google.com/search/docs/crawling-indexing/robots/create-robots-txt) in the robots.txt file. If a robots.txt file does not exist, or it does not offer any sitemap directive, the crawler will try with `/sitemap.xml` under the root of your domain.

> [!WARNING] Ensure the URLs in your sitemaps match your domain!
> Any link to domains different than the one configured as the "Website frontend URL" in your build trigger settings will be ignored by the bot.

### Using a User-Agent custom suffix

For each search index, you can specify a custom suffix to be added to the standard User-Agent: for example, if you define "Docs", our crawler will use `DatoCmsSearchBotDocs` as a User-Agent.

By using a custom User-Agent and a custom-tailored robots.txt, you can restrict the crawling to specific subsets of your website and therefore provide even more refined search experiences.

Here is an example: with the following robots.txt, you can create two separate search indexes (one with the suffix "Docs", the other with the suffix "Blog") for the documentation and the blog sections of a website:

```plaintext
User-agent: DatoCmsSearchBotDocs
Allow: /docs/
Disallow: /

User-agent: DatoCmsSearchBotBlog
Allow: /blog/
Disallow: /
```

### Language Detection

Through the HTML global `lang` attribute present on a page — or language-detection heuristics, if the attribute is missing — we detect the language of every crawled page, so that indexing will happen with proper stemming.

That is, if the visitor searches for "cats", we'll also return results for "cat", "catlike", "catty", etc.

### Plain HTML only

The crawler does not execute JavaScript on the spidered pages, it only parses plain HTML. If your website is a Single Page App, you'll need to setup [pre-rendering](https://www.netlify.com/blog/2016/11/22/prerendering-explained/) to make it readable by our bot.

### Excluding content from indexing

To give your users the best experience, it's often useful to instruct DatoCmsSearchBot to exclude certain parts of your pages from indexing — ie. website headers and footers. Those sections are repeated in every page, thus can only degrade your search results.

To do that, you can simply add a `data-datocms-noindex` attribute to the HTML elements of your page you want to exclude: everything cointained in those elements will be ignored during indexing.

```html
<body>
  <div class="header" data-datocms-noindex>
    ...
  </div>
  <div class="main-content">
    ...
  </div>
  <div class="footer" data-datocms-noindex>
    ...
  </div>
</body>
```

### Crawling time

The time needed to finish the crawling operation depends on the number of pages in your website and your hosting's performances, but normally it's about ~20 indexed pages/sec.

---

# DatoCMS Site Search — Perform searches via API

Source [docs]: https://www.datocms.com/docs/site-search/base-integration.md

**Important:** Before you can perform a site search request via API, you must first set up a search index. Please see [Configuration](/docs/site-search/configuration.md) for setup instructions.

Once you've configured the search index, we can start performing search requests to our API to present relevant results to your visitors.

#### Obtaining an API token

To do that, first you need to generate an API token with the proper permissions. Go to *Settings \> Roles* and create a new role with just the *Can perform Site Search API calls* permission checked.

(Image content)

You can then create a new API token associating it with the role you just created:

(Image content)

Awesome! Let's test if everything is working by making our first search request.

#### Performing searches

Our Content Management API offers a [REST endpoint to perform search requests](/docs/content-management-api/resources/search-result/instances.md): please refer to its reference page to know which parameter you can pass.

The easiest way to use the endpoint is through our [JavaScript clients](/docs/content-management-api/using-the-nodejs-clients.md). Depending on the environment where you're running your code, you can install the `@datocms/cma-client-browser` or the `@datocms/cma-client-node` npm package:

```javascript
import { buildClient } from '@datocms/cma-client-browser';

const client = buildClient({ apiToken: '<YOUR_API_TOKEN>' });

const { data: searchResults, meta } = await client.searchResults.rawList({
  filter: {
    fuzzy: true,
    query: 'term to search',
    search_index_id: '4324',
    locale: 'it',
  },
  page: {
    limit: 20,
    offset: 0,
  },
});

console.log(`Total results: ${meta.total_count}`);
console.log(JSON.stringify(searchResults, null, 2));
```

> [!WARNING] Make sure to always specify the `search_index_id` parameter!
> If you have multiple search indexes, you need to specify the `filter: { search_index_id }` parameter. You can find the search index ID as the last number of the search index URL on the dashboard or in the upper right corner of the search index settings. While, technically speaking, you can omit this parameter if you only have one build trigger, it is strongly suggested that you always pass it.

> [!WARNING] Want more results? Activate fuzzy search
> When the `fuzzy` parameter is passed, our search engine will find strings that *approximately* match the query provided. For instance, strings like *florence* will be matched, even if the query is *flor****a****nce*.

Let's take a look at how a search result looks like:

```json
{
  "type": "search_result",
  "id": "12adNIIB8rFJF1DoTgCk",
  "attributes": {
    "title": "Florence Apartments for Rent | Long Term Student Accommodation Rentals",
    "body_excerpt": "Finding a place to live while planning to study abroad in Florence can be both exciting and challenging. With this in mind, Housing in Florence assists you in finding conveniently-located housing based...",
    "url": "http://www.website.com/some-page",
    "score": 11.3,
    "highlight": {
      "title": [
        "[h]Florence[/h] Apartments for Rent | Long Term Student Accommodation Rentals"
      ],
      "body": [
        "All our student accommodation and apartments in [h]Florence[/h] are fully"
      ]
    }
  }
}
```

Each search result contains the `title` and `url` of the page, along with the first 200 characters of its content (`body_excerpt`). In the `highlight` attribute you can also find the parts of the title/page content that match the query, with the specific occurrence of the query highlighted in a `[h]` tag.

You can easily replace the `[h]` tag with a proper HTML tag of your choice like this:

```javascript
function highlightMatches(string, highlight) {
  return string.replace(/\[h\](.+?)\[\/h\]/g, function(a, b) {
    var div = document.createElement('div');
    div.innerHTML = highlight;
    div.children[0].innerText = b;
    return div.children[0].outerHTML;
  });
}

highlightMatches('[h]Florence[/h] Apartments for Rent', '<span class="highlight"></span>');
// -> '<span class="highlight">Florence</span> Apartments for Rent'
```

---

# DatoCMS Site Search — React search widget

Source [docs]: https://www.datocms.com/docs/site-search/widget.md

In addition to the [low-level API request](/docs/site-search/base-integration.md) presented in the previous section, our [`react-datocms`](https://github.com/datocms/react-datocms/blob/master/docs/site-search.md)package also includes a **React hook** that you can use to render a full-featured Site Search widget on your website.

> [!POSITIVE] You're in charge of the UI!
> The hook only handles the form logic: you are in complete and full control of how your form renders down to the very last component, class or style.

### Setup

First of all, install the required npm packages in your React project:

Terminal window

```bash
npm install --save @datocms/cma-client-browser
```

You can then use the `useSiteSearch` hook like this:

```jsx
import { useSiteSearch } from 'react-datocms';
import { buildClient } from '@datocms/cma-client-browser';

const client = buildClient({ apiToken: 'YOUR_API_TOKEN' });

const { state, error, data } = useSiteSearch({
  client,
  searchIndexId: '7497',
  // optional: you can omit it you only have one locale, or you want to find results in every locale
  initialState: { locale: 'en' },
  // optional: to configure how to present the part of page title/content that matches the query
  highlightMatch: (text, key, context) =>
    context === 'title' ? (
      <strong key={key}>{text}</strong>
    ) : (
      <mark key={key}>{text}</mark>
    ),
  // optional: defaults to 8 search results per page
  resultsPerPage: 10,
});
```

Please follow the `react-datocms` documentation to read more about at the [configuration options](https://github.com/datocms/react-datocms/blob/master/docs/site-search.md#initialization-options) and the [data returned by the hook](https://github.com/datocms/react-datocms/blob/master/docs/site-search.md#returned-data).

### Complete example

The following example uses the [`react-paginate`](https://www.npmjs.com/package/react-paginate) npm package to simplify the handling of pagination. You can build your own pagination using the `data.totalPages` property to get the total number of pages, `state.page` to get the current page, and `state.setPage(page)` to trigger a page change.

```jsx
import { useState } from 'react';
import { buildClient } from '@datocms/cma-client-browser';
import ReactPaginate from 'react-paginate';
import { useSiteSearch } from 'react-datocms';

const client = buildClient({ apiToken: 'YOUR_API_TOKEN' });

function App() {
  const [query, setQuery] = useState('');

  const { state, error, data } = useSiteSearch({
    client,
    initialState: { locale: 'en' },
    searchIndexId: '7497',
    resultsPerPage: 10,
  });

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          state.setQuery(query);
        }}
      >
        <input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <select
          value={state.locale}
          onChange={(e) => {
            state.setLocale(e.target.value);
          }}
        >
          <option value="en">English</option>
          <option value="it">Italian</option>
        </select>
      </form>
      {!data && !error && <p>Loading...</p>}
      {error && <p>Error! {error}</p>}
      {data && (
        <>
          {data.pageResults.map((result) => (
            <div key={result.id}>
              <a href={result.url}>{result.title}</a>
              <div>{result.bodyExcerpt}</div>
              <div>{result.url}</div>
            </div>
          ))}
          <p>Total results: {data.totalResults}</p>
          <ReactPaginate
            pageCount={data.totalPages}
            forcePage={state.page}
            onPageChange={({ selected }) => {
              state.setPage(selected);
            }}
            activeClassName="active"
            renderOnZeroPageCount={() => null}
          />
        </>
      )}
    </div>
  );
}
```

---

# DatoCMS Site Search — Vue search widget

Source [docs]: https://www.datocms.com/docs/site-search/vue-search-widget.md

In addition to the [low-level API request](/docs/site-search/base-integration.md) presented in the previous section, our [`vue-datocms`](https://github.com/datocms/vue-datocms/tree/master/src/composables/useSiteSearch)package also includes a Vue composable ready for rendering a full-featured Site Search widget on your website.

> [!POSITIVE] You're in charge of the UI
> The composable only handles the logic: you are in complete control of how the form and the list of results render, down to the last component, class or style.

### Setup

First of all, install the required npm packages in your Vue project:

Terminal window

```bash
npm install --save @datocms/cma-client-browser vue-datocms
```

You can then use the `useSiteSearch` composable like this:

```javascript
import { useSiteSearch } from 'vue-datocms';
import { buildClient } from '@datocms/cma-client-browser';

const client = buildClient({ apiToken: 'YOUR_API_TOKEN' });

const { state, error, data } = useSiteSearch({
  client,
  searchIndexId: '7497',
  // optional: by default fuzzy-search is not active
  fuzzySearch: true,
  // optional: you can omit it if you only have one locale, or you want to find results in every locale
  initialState: { locale: 'en' },
  // optional: defaults to 8 search results per page
  resultsPerPage: 10,
})
```

Please follow the `vue-datocms` documentation to read more about at the [configuration options](https://github.com/datocms/vue-datocms/tree/master/src/composables/useSiteSearch#initialization-options) and the [data returned by the hook](https://github.com/datocms/vue-datocms/tree/master/src/composables/useSiteSearch#returned-data).

### Complete example

The following example shows a search page, including a very simple home-made pagination. You can build more advanced pagination widgets using the `data.totalPages` property to get the total number of pages, `state.page` to get the current page, and `state.page = pageNumber` to trigger a page change.

```html
<script setup lang="ts">

import { useSiteSearch } from 'vue-datocms'

import { buildClient } from '@datocms/cma-client-browser';

const client = buildClient({ apiToken: 'YOUR_API_TOKEN' });

const { state, error, data } = useSiteSearch({
  client,
  searchIndexId: '7497',
  // optional: by default fuzzy-search is not active
  fuzzySearch: true,
  // optional: you can omit it you only have one locale, or you want to find results in every locale
  initialState: { locale: 'en' },
  // optional: defaults to 8 search results per page
  resultsPerPage: 9,
})

</script>

<template>
  <div>
    <div class="bg-slate-200 py-4">
      <div class="container mx-auto">
        <input class="py-3 px-5 block w-full border-gray-200 rounded-full text-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400" type="text" v-model="state.query" placeholder="Search: try something like &quot;vue&quot; or &quot;dato&quot;... " />
      </div>
    </div>
    <div class="bg-slate-100 py-4">
      <div class="container mx-auto py-4">
        <h1>{{ data.totalResults}} results</h1>
      </div>
      <div class="container mx-auto py-4 grid grid-cols-3 gap-4" v-if="data">
        <div v-for="result in data.pageResults" class="py-4">
          <div class="py-1">
            <a :href="result.url">
              <strong v-if="result.titleHighlights.length > 0">
                <template v-for="highlight in result.titleHighlights" class="py-1">
                  <template v-for="piece in highlight">
                    <mark v-if="piece.isMatch">{{ piece.text }}</mark>
                    <template v-else>{{ piece.text }}</template>
                  </template>
                </template>
              </strong>
              <strong v-else>{{ result.title }}</strong>
            </a>
          </div>
          <div v-for="highlight in result.bodyHighlights" class="py-1">
            <template v-for="piece in highlight">
              <mark v-if="piece.isMatch">{{ piece.text }}</mark>
              <template v-else>{{ piece.text }}</template>
            </template>
          </div>
          <details>
            <summary>Raw results</summary>
            <pre><code class="block whitespace-pre overflow-x-scroll">{{ JSON.stringify(result.raw, null, 2) }}</code></pre>
          </details>
        </div>
      </div>
      <div class="container mx-auto py-4">
        <div class="flex">
          <button v-if="state.page > 0" @click="state.page = state.page - 1" class="flex items-center px-4 py-2 mx-1 text-gray-700 transition-colors duration-300 transform bg-white rounded-md dark:bg-gray-800 dark:text-gray-200 hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white dark:hover:text-gray-200">
            Previous
          </button>
          <button v-if="state.page < data.totalPages" @click="state.page = state.page + 1" class="flex items-center px-4 py-2 mx-1 text-gray-700 transition-colors duration-300 transform bg-white rounded-md dark:bg-gray-800 dark:text-gray-200 hover:bg-blue-600 dark:hover:bg-blue-500 hover:text-white dark:hover:text-gray-200">
            Next
          </button>
        </div>
      </div>
    </div>
  </div>
</template>
```

---

# Streaming Videos — How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming

Source [docs]: https://www.datocms.com/docs/streaming-videos/how-to-stream-videos-efficiently.md

This guide will walk you through the process of streaming videos using DatoCMS. We'll cover everything from uploading and encoding to implementation and troubleshooting, and help you select the most efficient and cost-effective methods to enhance your video delivery.

### Video streaming options

DatoCMS offers two main ways to stream videos:

1.  Adaptive bitrate streaming using HTTP Live Streaming (HLS)
    
2.  Serving raw MP4 files for direct download
    

These are described in more detail below.

#### **Option 1 (recommended): Adaptive Bitrate streaming through Mux**

We offer powerful video streaming capabilities thanks to our integration with [Mux](https://www.mux.com/), a leading cloud encoding platform for on-demand streaming video.

Adaptive Bitrate uses the HLS (HTTP Live Streaming) protocol and provides **plenty of benefits**:

**✅ Optimal viewing experience**: The video quality adapts automatically to the user’s connection, ensuring minimal buffering and seamless playback.

**✅ Bandwidth efficiency**: It conserves data by reducing the video quality for users with slower connections, optimizing bandwidth usage.

**✅ Improved performance**: Streaming is done through an optimized Content Delivery Network (CDN) designed for video, which ensures faster video delivery and lower latency.

To implement this method on your frontend, you have two options.

**Use our Video Player component**

We offer an easy-to-use `<VideoPlayer/>` component for:

-   [React/Next.js/React Router](/docs/next-js/displaying-videos.md)
-   [Vue, Nuxt](/docs/nuxt.md)
    
-   [Svelte/SvelteKit](/docs/svelte/displaying-videos.md)
    

Our player is a thin wrapper around [Mux's own implementation](https://www.mux.com/player), but ours is specifically designed for DatoCMS and makes it easy to display videos straight from your GraphQL queries.

**Implement a HLS player yourself**

If you prefer more control or are using a different framework, you can implement the video player manually using the data returned from the API:

1.  Use the `muxPlaybackId` to construct the HLS streaming URL: `https://stream.mux.com/{PLAYBACK_ID}.m3u8`
    
2.  Implement a video player that supports HLS (e.g., video.js, Plyr, or hls.js)
    
3.  For fallback support, use the provided MP4 URLs (high, medium, and low quality)
    

#### **Option 2: Serving the MP4 file directly (NOT recommended)**

An alternative method is serving the MP4 file directly from `datocms-assets.com` using an HTML `<video>` tag. Although this is an option, it is generally not recommended **due to several drawbacks**:

**❌ High bandwidth costs**: Serving the raw MP4s will generate substantial traffic, leading to increased bandwidth consumption and higher costs. This is especially the case for autoloading videos (like a hero or background video) , which will typically consume several megabytes of bandwidth for every visitor, regardless of their device and bandwidth.

**❌ No quality control**: Unlike adaptive bitrate streaming, the video does not adjust its quality dynamically based on the viewer’s internet speed, leading to poor streaming quality.

**❌ Lack of CDN optimization**: Direct MP4 serving does not benefit from CDN optimization, resulting in higher latency and slower load times.

### Best practices for Video Streaming

#### Use HLS Streaming

We strongly recommend using HLS (HTTP Live Streaming) as it provides a superior method for managing traffic and reducing costs. HLS delivers a streaming URL (streamingUrl) that adjusts video quality according to the viewer’s network conditions, ensuring efficient and smooth playback.

#### Blocking Raw Video URLs

To prevent the serving of raw video URLs and reduce unnecessary bandwidth usage, configure your project settings to [block direct access to video files](/docs/asset-api/asset-cdn-settings.md#block-serving-raw-videos) via the Asset CDN. This step ensures videos are streamed efficiently using the provided HLS URLs.

Blocking direct access to videos has been the default setting for new DatoCMS projects since March 2024, but older projects need to explicitly opt-in to enable blocking.

### How Video Streaming is billed

Understanding how streaming is billed is crucial for managing costs effectively.

For **HLS Streaming**, billing is based on the number of streaming minutes. This means you are charged for the actual time viewers spend watching your videos, regardless of their bandwidth usage. This makes for predictable, viewership-based billing, and will usually be substantially lower in cost.

In contrast, **MP4 Serving** is billed based on bandwidth usage. This method measures the total amount of data transferred when users watch your videos. Since the video is served in its entirety without adjusting for quality, this leads to higher data usage and, consequently, higher costs, especially if you have a large audience and/or if the video files are large.

For all the details, you can read more on [How overages are managed](/docs/plans-pricing-and-billing/overcharges-on-api-and-bandwidth.md).

### Monitoring your Video Streaming usage

DatoCMS provides tools to help you monitor your project's streaming usage, allowing you to stay on top of your costs and usage patterns. The Project Usages page, part of the "Project settings" area in the CMS, offers detailed insights into your usage metrics.

Here, you can see a breakdown of how much you are being charged for streaming minutes (HLS) and bandwidth (MP4 serving).

To determine which type of charge you are incurring, look under the specific categories:

-   **Streaming Minutes**: This section shows the total time your videos have been streamed using HLS.
    

(Image content)

-   **Bandwidth**: This section details the data transferred for videos served directly via MP4. You can check the **top assets by traffic**, as well as the **top referrers for assets**, while the graph highlights in purple all the traffic generated by your projects' assets.
    

(Image content)

### Troubleshooting

#### Reducing overages

If you notice a spike in your overages and usage metrics, it’s essential to investigate and address the root cause. A common issue is high bandwidth usage due to serving MP4 videos directly. To troubleshoot this:

-   Audit your site for `<video>` tags and switch them to HLS streaming if they are currently set to serve MP4 files directly.
-   Use the network tab in your browser’s developer tools to ensure videos are being served from `stream.mux.com` and not `datocms-assets.com`
    

#### Looping auto-play background videos

Here are some suggestions for optimizing the scenario where you want to use a looping video as a background in your page layout:

-   **Use Short Clips**: Keep the video short enough to fit within the browser’s memory cache (typically less than 10 seconds). This prevents Mux from re-downloading the video each time it loops, reducing streaming costs.
-   **Optimize Quality and Size**: Balance video quality with file size to minimize data usage without sacrificing user experience. In some cases, using a lower resolution MP4 might be more cost-effective than HLS streaming if the browser can reliably cache it.
    
-   **Alternative Hosting**: Consider hosting the video on a third-party CDN if their bandwidth costs are lower. This approach can bypass both Mux and DatoCMS CDNs, potentially reducing expenses further, especially if you have a preexisting contract with them that includes high amounts of bandwidth. You would be billed separately by the third-party host.
-   **Static Asset on Your Frontend**: If your file is small enough and you have a sufficient plan with your frontend's current host & CDN, consider adding the file to your frontend repo and serving directly from there, alongside your favicons, decorative images, fonts, etc. This is similar to the previous option of hosting the video on an alternative host, but this saves you the trouble of needing a seperate account & plan just for hosting these videos. Please check with your frontend host to see how this would affect your billing.

---

# Streaming Videos — Streaming Video Analytics with Mux Data

Source [docs]: https://www.datocms.com/docs/streaming-videos/streaming-video-analytics-with-mux-data.md

If you've seen our article on [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md) , you know that serving HLS (HTTP Live Streaming) video can provide your visitors with a good streaming experience and help you control costs at the same time. These HLS videos are served through our video CDN partner, [Mux.](https://www.mux.com/)

Mux provides a detailed frontend analytics service for your streaming videos: [Mux Data](https://www.mux.com/data).

Some quick setup on your frontend is required before analytics data will be available. This page details how to send analytics to Mux Data using our `<VideoPlayer/>` component for [React](/docs/next-js/displaying-videos.md), [Vue](/docs/nuxt/displaying-videos.md), and [Svelte](/docs/svelte/displaying-videos.md). Our component is just a thin wrapper around [`mux-player`](https://www.mux.com/player), making it easier to use with our [Content Delivery API](/docs/content-delivery-api.md) output while still using largely the same parameters as the original.

## What is Mux Data and how is it different from the DatoCMS Project Usages screen?

In your Project Usages section, DatoCMS provides basic information about your HLS streams. We show you the # of streamed minutes per file:

(Image content)

If you want more detailed video analytics, you'll have to use a third-party solution. Luckily, Mux already provides such a service, [Mux Data](https://www.mux.com/data).

Once set up, Mux Data provides you detailed information on your video views:

(Image content)

Mux Data Overview

### What metrics are tracked by Mux Data?

Mux Data tracks technical device details (user agents, OS and player details, codecs, etc.), visitor engagement metrics (unique viewers, play times, percentage completions, etc.) and much more.

> [!NOTE] Full list of Mux Data Metrics
> For the full, updated list of metrics tracked by Mux Data, please see [Mux Data: Technical Specs](https://www.mux.com/data#TechSpecs)

### How much does Mux Data cost? How am I billed for it?

Mux Data is a **separate service** offered by Mux directly. It is not a part of your DatoCMS subscription and does not affect your billing here in any way.

As of September 2025, Mux Data is free up to 100k views/month. No credit card is needed to sign up.

You can see more pricing details at [https://www.mux.com/pricing/data](https://www.mux.com/pricing/data).

If you choose to subscribe to a Mux Data paid plan, you will be paying Mux directly, under your separate account with them.

## How to use Mux Data with DatoCMS

It's a simple process: Sign up for a [Mux Data account](https://dashboard.mux.com/signup?type=data) (it's free and easy), then add your Mux Data env key as a parameter to your DatoCMS `<VideoPlayer/>` or the official `<mux-player/>` component.

Here is the step-by-step:

##### Use HLS Streaming

Ensure that you're serving videos using HLS (HTTP Live Streaming), not the raw .MP4s. We have a guide about this: [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md)

##### Set up <VideoPlayer/\> Component

If you're not yet using our `<VideoPlayer/>` component, follow one of these guides to set it up on your frontend:

-   [<VideoPlayer/\> component for React & Next.js](/docs/next-js/displaying-videos.md)
-   [<VideoPlayer/\> component for Vue & Nuxt](/docs/nuxt/displaying-videos.md)
    
-   [<VideoPlayer/\> component for Svelte & SvelteKit](/docs/svelte/displaying-videos.md)
    

If you prefer, you can also use the official `mux-player` instead: [https://www.mux.com/docs/guides/mux-player-web](https://www.mux.com/docs/guides/mux-player-web)

Our `<VideoPlayer/>` components are just a thin wrapper over the official player, sharing most of the same parameters.

##### Sign up for Mux Data

Then, [sign up for a Mux Data account](https://dashboard.mux.com/signup?type=data). This happens outside of DatoCMS, on the Mux Data site itself. This is completely separate from your DatoCMS account.

##### Copy your Mux Data env key

After you've signed up, log in with your credentials. In the Mux dashboard, hover over your team name and go to the "All Environments" screen:

(Image content)

Mux "All Environments" dropdown

You should see one or more environments with their env keys in the bottom right. Click to copy one of them (for the environment you want to monitor, probably "Production"):

(Image content)

Mux Environment Keys

Please note that these Mux environments **are not related to your DatoCMS project environments**. They are just internal names used by Mux Data, which you can assign to your different frontend environments if you have them.

##### Add your Mux Data env key to the <VideoPlayer/\> component

Now that you have your env key (it should be an 8-digit alphanumeric key like `abcd1234`), you can simply supply it to your `<VideoPlayer/>` component:

```tsx
<VideoPlayer
  envKey={myOwnMuxDataEnvKey} // Your own env key from your Mux Data dashboard at https://dashboard.mux.com/environments
  disableTracking={false} // We normally default it to true, so you have to explicitly enable it
  data={yourVideoData} // The video object you get back from our CDA. Must include `muxPlaybackId`.
  // debug={true} // (Optional) Shows you analytics events in the browser console
  // {...rest} // Anything else you needed to add. See https://www.mux.com/docs/guides/player-api-reference
/>
```

For an example, please see [this Stackblitz demo](https://stackblitz.com/~/github.com/arcataroger/mux-data-test).

*See also:* [*Mux Player API Reference*](https://www.mux.com/docs/guides/player-api-reference) *for a list of all accepted parameters*

##### Viewing your Mux Data Analytics

If the setup succeeded, try viewing your video for a few seconds. Analytics should start trickling into Mux Data. Now, just return to the Mux dashboard (`dashboard.mux.com`) and choose your environment again. You should start seeing analytics!

(Image content)

Mux Data Dashboard

If you enabled `debug={true}`, you'll also see these logging attempts in the browser console as they're sent, which can be especially useful if the analytics *aren't* showing up as expected (probably because of adblock; see the troubleshooting section below).

## Documentation and Troubleshooting

### Official Documentation

**Mux Data:** Because Mux Data is a service offered by our partner and not DatoCMS directly, please see the official Mux Data documentation for detailed usage information: [**Introduction to Mux Data**](https://www.mux.com/docs/guides/data).

Our`**<VideoPlayer/>**` component: Please see the readmes for the [**React**](https://github.com/datocms/react-datocms)**,** [**Vue**](https://github.com/datocms/vue-datocms)**,** or [**Svelte**](https://github.com/datocms/datocms-svelte/) packages.

The original `**mux-player**` that our component wraps: [**Mux Player for Web documentation**](https://www.mux.com/docs/guides/mux-player-web) and [**API reference**](https://www.mux.com/docs/guides/player-api-reference). Note that the React version has similar but slightly different parameters compared to the web component version.

### Not seeing anything in your Mux Data? **It may be ad-blocked**

If you're sure you've followed the above setup steps and still aren't seeing any analytics in Mux Data after a few minutes, **the most likely cause is ad-blocking.** Mux Data is an entirely clientside script that runs in the user's browser, and many adblockers (like uBlock Origin) will block it by default.

For testing, please disable any browser extensions and/or watch the video in an Incognito/Private window.

For production usage, you should also keep in mind that your own visitors may also be using ad blockers. Thus, they may not send you any Mux Data events at all, and the metrics you do see may be skewed towards users who aren't blocking ads. (The same would apply to any other clientside user analytics tracking).

### Seeing different numbers in Mux Data vs your DatoCMS Project Usages?

Because Mux Data is clientside and runs in your users' browsers, it may be affected by ad blocking, network policies, etc. These can all prevent analytics events from being successfully collected.

In comparison, the streaming minutes measured in your DatoCMS Project Usages is an authoritative serverside log tracked directly by Mux's CDN.

Thus, for billing purposes, the **DatoCMS Project Usages section is considered the authoritative source of truth for accurately counting streamed minutes.**

Still, Mux Data — even though it only tracks some percentage of your viewers and not all — can provide many additional insights into your viewership and engagement. Using both together can help you optimize your technical delivery and editorial engagement. Our serverside records tell you exactly how much time each video was watched, while Mux Data gives you much more fine-grained metrics on their delivery and engagement.

### Need help?

Although Mux Data is offered by our partner Mux, we here at DatoCMS still very much value you as our shared customer 🙂

As such, if you run into issues with any of this, please feel free to [reach out to our support team](https://www.datocms.com/support.md#form?topics=technical-support%2Fgeneral-request) or [check our forum](https://community.datocms.com/c/support/18) and we'll do our best to help!

Or if you're sure it's something out of our control, you can also reach out to Mux directly: [Open a ticket with Mux](https://www.mux.com/support/human)

---

# DatoCMS CLI — CLI Overview

Source [docs]: https://www.datocms.com/docs/cli.md

Most programmatic work with DatoCMS happens through the [Content Management API](/docs/content-management-api.md), either via direct HTTP requests or the [JavaScript CMA client](/docs/content-management-api/using-the-nodejs-clients.md). On top of that API, the **DatoCMS CLI** (`datocms`) packages the recurring project workflows into ready-made terminal commands. You install it as a dev dependency in your repo, link it to a project once, and from there you can manage environments, run schema migrations, generate TypeScript types, toggle maintenance mode, import from other CMSs, and call the Content Management API directly, all without leaving the terminal.

## What you can do

Once installed and linked to a project, the CLI lets you:

-   **Set up a repo that talks to DatoCMS**: install, OAuth login, and link the project to a `datocms.config.json` file.
-   **Run schema migrations**: scaffold and execute versioned migration scripts that evolve your project's content model in a controlled way.
    
-   **Manage environments**: fork sandbox environments, promote them to primary, rename, or destroy them.
-   **Generate TypeScript types**: produce a typed schema of your project for use in your application code and migrations.
    
-   **Toggle maintenance mode**: lock the project during deploys or other sensitive operations.
-   **Import content from other CMSs**: official plugins for Contentful and WordPress.
    
-   **Manage multiple projects**: use profiles to keep blueprint and client projects in sync from the same repo.
    

## How the CLI relates to MCP Server and Agent Skills

Two other tools build on top of the CLI or complement it:

-   [Agent Skills](/docs/agent-skills.md) are a knowledge package built on top of this CLI. Install them in your editor and an AI coding agent (Claude Code, Codex, Cursor, …) gains the ability to do everything this section describes, plus content modelling guidance, frontend integration patterns, and plugin authoring expertise. The Skills handle CLI bootstrap for you, guiding you through install, `login`, and `link` when needed.
-   [MCP Server](/docs/mcp-server.md) is the right choice when there is no local terminal to work in (for example, an editor or product manager interacting with DatoCMS from a web-based AI assistant).
    

If you are a developer working in a terminal, the CLI is the starting point. Install Agent Skills on top of it to unlock agentic workflows.

## Install the CLI

Install the `datocms` package as a dev dependency of your repo:

Terminal window

```bash
npm install --save-dev datocms
```

## Authenticate via OAuth

The recommended way to authenticate is via OAuth: your browser opens, you log in with your DatoCMS account, and the CLI stores credentials locally. No tokens to copy and paste.

Terminal window

```bash
npx datocms login
```

Credentials are saved to `~/.config/datocms/credentials.json`. You can verify your identity at any time:

Terminal window

```bash
npx datocms whoami
```

`whoami` is also useful as a quick health-check that the CLI is set up correctly; coding agents tend to call it as the first step when operating on an unfamiliar repo.

## Link the current directory to a project

Linking generates a `datocms.config.json` configuration file in the current directory and avoids having to repeat options for every command you run:

```plaintext
$ npx datocms link

✔ Choose a workspace › My organization
✔ Search and select a project › My project
✔ Directory where script migrations will be stored ./migrations
✔ API key of the DatoCMS model used to store migration data schema_migration
Writing "datocms.config.json"... done
```

Once linked, every CLI command in this directory automatically resolves an API token for the linked project using your OAuth credentials. No need to set environment variables.

The generated `datocms.config.json` file will look similar to this:

```json5
{
  "profiles": {
    "default": {
      "logLevel": "NONE",
      "siteId": "12345", // The linked DatoCMS project ID
      "organizationId": "67890", // The organization the project belongs to
      "migrations": {
        "directory": "./migrations",
        "modelApiKey": "schema_migration",
        "template": "",
        "tsconfig": ""
      }
    }
  }
}
```

Add the config file to your Git repository so your collaborators and CI pipelines can reuse it:

Terminal window

```bash
git add datocms.config.json
git commit -m "Add datocms.config.json file"
```

> [!PROTIP] Pro tip: Need to manage multiple DatoCMS projects from the same repo?
> You can set up additional profiles with `datocms link --profile=<NEW_PROFILE_NAME>`, then specify which profile to use with the `--profile` flag (or by setting a `DATOCMS_PROFILE` environment variable). See [Profiles and multi-project setup](/docs/cli/profiles-and-multi-project-setup.md) for the full picture.

---

# DatoCMS CLI — Environment, migration and maintenance commands

Source [docs]: https://www.datocms.com/docs/cli/environment-migration-and-maintenance-commands.md

This chapter is a **command reference** for three groups of CLI commands that, together, form the lifecycle of a DatoCMS project: environments let you create isolated sandboxes, migrations evolve the schema in a controlled way, and maintenance mode locks the primary environment while sensitive changes go through.

The **end-to-end deploy workflow** that uses these commands (*fork → migrate → maintenance:on → promote → maintenance:off*) is documented in [Apply migrations to primary environment](/docs/scripting-migrations/apply-migrations-to-primary-environment.md), with rationale, edge cases, and CI patterns. This page just lists what each command does and the flags that matter.

## Environment commands

DatoCMS lets you fork any environment into a new sandbox, work on it in isolation, and then promote it back to primary when ready. Five CLI commands cover the lifecycle:

Terminal window

```bash
npx datocms environments:list
npx datocms environments:fork <SOURCE_ENV> <NEW_ENV>
npx datocms environments:promote <ENV_ID>
npx datocms environments:rename <ENV_ID> <NEW_ENV_ID>
npx datocms environments:destroy <ENV_ID>
```

-   `environments:list`: list every environment in the project (primary plus sandboxes).
-   `environments:fork`: create a sandbox from an existing environment. Add `--fast` to skip the data copy when you only care about the schema, or `--force` to overwrite an existing environment with the same name.
    
-   `environments:promote`: promote a sandbox to primary. The previous primary becomes a sandbox.
-   `environments:rename`: rename an environment.
    
-   `environments:destroy`: delete a sandbox.
    

For the underlying concept (primary vs sandbox environments, when to fork, how promotion works) see [Primary and sandbox environments](/docs/general-concepts/primary-and-sandbox-environments.md). For the recommended workflow when applying changes (fork → migrate → promote) see [Safe iterations using environments](/docs/scripting-migrations/safe-iterations-using-environments.md).

> [!NOTE] Automating environments in CI
> Environment commands are commonly automated. Typical patterns: forking a per-PR preview environment when a pull request opens, and destroying it when the pull request closes. Combine `environments:fork` with `--force` and `--fast` to make them re-runnable in a CI step.

## Migration commands

Migrations are versioned scripts that evolve your project's schema. The CLI exposes two commands:

Terminal window

```bash
npx datocms migrations:new <NAME>
npx datocms migrations:run
```

-   `migrations:new`: scaffold a new migration script. Defaults to TypeScript when a `tsconfig.json` is present; pass `--js` to force JavaScript. Add `--autogenerate=<env>` to generate a script that diffs the current project against another environment, or `--schema=Article,Author` to inline the TypeScript schema of selected models into the new script.
-   `migrations:run`: apply all pending migrations. By default, it forks the primary environment into a sandbox called `<primary>-post-migrations`, runs the migrations there, and stops (it does not promote). Key flags:
    
    -   `--source` and `--destination`: the environment to fork and the name of the new sandbox. Defaults: source is primary, destination is `<source>-post-migrations`.
        
    -   `--in-place`: run migrations in the `--source` environment directly, without forking. Mutually exclusive with `--destination`.
        
    -   `--allow-primary`: required when `--in-place` would target the primary environment. Use only for strictly additive migrations: there is no rollback if the run fails partway through. The recommended workflow remains fork, migrate, promote.
        
    -   `--dry-run`: simulate the run without writing anything to the project.
        
    -   `--fast-fork`: use a fast (schema-only) fork instead of a full one. The source is locked to writes while the fork is in progress. `--force` lets the fast fork start even when there are active editing sessions on the source.
        
    
    For config overrides (`--migrations-dir`, `--migrations-model`, `--migrations-tsconfig`) and the full flag list, run `npx datocms migrations:run --help`.
    

For everything migration-related (writing migration scripts, the recommended workflow, applying to primary), see the [Scripting Migrations section](/docs/scripting-migrations/introduction.md). In particular: [Write and test migration scripts](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md) and [Apply migrations to primary environment](/docs/scripting-migrations/apply-migrations-to-primary-environment.md).

## Maintenance mode commands

Maintenance mode prevents content changes to the primary environment, both via the Content Management API and from collaborators editing in the dashboard. Use it during deploys, schema promotions, or any operation where concurrent writes would be unsafe.

Terminal window

```bash
npx datocms maintenance:on
npx datocms maintenance:off
```

-   `maintenance:on`: enable maintenance mode. Pass `--force` to enable it even when other users are currently editing.
-   `maintenance:off`: disable maintenance mode.
    

Maintenance mode is typically wrapped around an `environments:promote`, so that no editor can write to primary while the promotion is in progress.

---

# DatoCMS CLI — Generating TypeScript types from your schema

Source [docs]: https://www.datocms.com/docs/cli/generating-typescript-schema.md

DatoCMS records don't have a predetermined structure, so the [JavaScript CMA client](/docs/content-management-api/using-the-nodejs-clients.md) cannot provide strict TypeScript types out of the box: every field comes back typed as `unknown`. To get full type-safety, auto-completions, and inline type hints in your editor, the CLI exposes a `schema:generate` command that reads your project's schema and emits a TypeScript definition file describing every model and block.

> [!POSITIVE] Generating is only half the story
> This page covers producing the schema file. For how to actually *use* it — passing a model marker as a generic for fully typed reads and writes, extracting the concrete type of an individual field, and narrowing a record or block down to a specific model — see [Type-safe development with TypeScript](/docs/content-management-api/resources/item.md#type-safe-development-with-typescript) in the CMA reference.

## The command

After [installing and linking the CLI](/docs/cli.md), generate the schema with:

Terminal window

```bash
npx datocms schema:generate schema.ts
```

The output is a TypeScript module you commit to your repo and import wherever you call the CMA client.

## What gets generated

For each model and block in your project, the command emits an `ItemTypeDefinition` **type** (used as a generic in CMA calls) and a runtime **constant** (used to reference the model's ID or build relationships):

```typescript
// schema.ts: generated by `npx datocms schema:generate`.
//
// Do not hand-edit. Re-run the generator whenever the schema changes.

import type { ItemTypeDefinition } from '@datocms/cma-client';

type EnvironmentSettings = { locales: 'en' | 'it' };

export type Article = ItemTypeDefinition<
  EnvironmentSettings,
  '76hhD-LaS5CM3NPJw0991w',
  {
    name: { type: 'string' };
    slug: { type: 'slug' };
    accent_color: { type: 'color' };
    sections: { type: 'rich_text'; blocks: ArticleSection };
  }
>;

export const Article = {
  ID: '76hhD-LaS5CM3NPJw0991w',
  REF: { type: 'item_type', id: '76hhD-LaS5CM3NPJw0991w' },
} as const;
```

The generated file then turns calls like `client.items.find<Schema.Article>(id)` into fully typed expressions. For the full set of patterns (using markers in API calls, extracting concrete field types, narrowing block unions, typing whole payloads), see [Type-safe development with TypeScript](/docs/content-management-api/resources/item.md#type-safe-development-with-typescript) in the CMA section.

## Two ways to generate the schema

The CLI lets you generate types in two complementary ways, depending on how you plan to use them.

###### Standalone: for your application code

The most common case: a single `schema.ts` file in your repo that your application code imports.

Terminal window

```bash
npx datocms schema:generate ./src/datocms-schema.ts
```

You typically commit this file and re-run the command whenever the project schema changes.

###### Inline: within a migration script

A migration script represents the schema at a **specific point in its history**: its job is to take the project from state *N* to state *N+1*. The standalone `schema.ts` file, on the other hand, always reflects the **latest** state of the project. If a migration written six months ago referenced `Schema.Article` from the global file, and `Article` has since been renamed or restructured, the migration would silently start type-checking against a schema it was never written for.

To keep a migration type-safe against its own historical view of the project, the CLI can **inline** the relevant model definitions directly into the migration file at scaffold time:

Terminal window

```bash
npx datocms migrations:new 'tweak articles' --schema=Article,Author
```

Use `--schema=all` to inline every model. The migration ends up self-contained: it carries its own snapshot of the schema, decoupled from the global `schema.ts`, and continues to type-check correctly even after the project schema evolves.

## Targeting a specific subset

Two flags narrow what gets generated:

-   `--item-types=product,article`: generate only the listed models. Useful when your project has hundreds of models and you only need a few in your app.
-   `--environment=my-sandbox`: generate from a specific sandbox environment instead of primary. Useful when previewing a schema change before promotion.
    

## Keeping types in sync

The generated file is only as fresh as the last `schema:generate` run. If your project schema changes (a new model, a renamed field) and you forget to regenerate, your TypeScript types will silently drift from the actual API shape.

A common pattern is to **automate the regeneration**: `npx datocms schema:generate` can be run by a git hook, or a nightly CI job (opening a pull request whenever the output differs). That way the types in your repo are always one merge away from being correct, and any drift is visible at code review time.

---

# DatoCMS CLI — Importing content from other CMSs

Source [docs]: https://www.datocms.com/docs/cli/importing-from-other-cms.md

The CLI is extensible via plugins. DatoCMS publishes two official plugins that import an existing project from another CMS into DatoCMS:

-   `@datocms/cli-plugin-contentful`: import a Contentful space.
-   `@datocms/cli-plugin-wordpress`: import a WordPress site.
    

Each plugin adds a new top-level command to the CLI once installed.

## Discover available plugins

To list the official plugins published by DatoCMS:

Terminal window

```bash
npx datocms plugins:available
```

## Install a plugin

Install a plugin into the current CLI installation:

Terminal window

```bash
npx datocms plugins:install @datocms/cli-plugin-contentful
```

After install, the plugin's commands become available alongside the built-in ones. The Contentful plugin adds `datocms contentful:import`; the WordPress plugin adds `datocms wordpress:import`.

## Run the import

Each plugin has its own dedicated guide that walks through the full import flow (required credentials, mapping options, asset handling, and known limitations):

-   [Import a space from Contentful](/docs/import-and-export/import-space-from-contentful.md)
-   [Import from WordPress](/docs/import-and-export/import-from-wordpress.md)
    

## Uninstall a plugin

When you no longer need a plugin (for example after a one-off import), remove it with:

Terminal window

```bash
npx datocms plugins:uninstall @datocms/cli-plugin-contentful
```

---

# DatoCMS CLI — CLI for AI coding agents

Source [docs]: https://www.datocms.com/docs/cli/cli-commands-for-ai-coding-agents.md

A handful of CLI commands exist primarily to make life easier for **AI coding agents** (Claude Code, Codex, Cursor, and similar tools) when they operate on a DatoCMS project from your editor. They are perfectly usable from a human terminal, but a human developer typically has better alternatives (the dashboard, the typed CMA client in their editor, the online API docs). When an agent is at the keyboard instead, these commands turn the CLI into a powerful surface for autonomous interaction with DatoCMS.

> [!PROTIP] Pro tip: Install Agent Skills to put these to work
> The most leveraged way to use these commands is to install the [DatoCMS Agent Skills](/docs/agent-skills.md). The Skills package the CLI together with the domain knowledge an agent needs to use it well — when to inspect the schema before generating types, how to scaffold a migration, when to fork an environment instead of working in primary, how to compose CMA calls without drifting from your project's conventions. The CLI is the engine; the Skills are the operator's manual.

## The agent-first commands

###### `schema:inspect`

Dump the structure of a DatoCMS project (models, blocks, fields, validators, appearance, fieldsets, nested blocks, relationships) as JSON or in a token-efficient format optimised for LLM consumption. Typically the first command an agent runs against an unfamiliar project; a human inspects schemas visually in the dashboard, or uses [`schema:generate`](/docs/cli/generating-typescript-schema.md) for a typed snapshot in code.

Terminal window

```bash
npx datocms schema:inspect
```

###### `cma:docs`

Browse the Content Management API reference from the terminal. Output is formatted for LLM consumption.

Terminal window

```bash
npx datocms cma:docs
npx datocms cma:docs item create
```

###### `projects:list`

List every DatoCMS project the authenticated account has access to. Supports fuzzy search by name or subdomain, and accepts `--workspace`, `--limit`, and `--json` flags. Agents invoke it during bootstrap to discover the `siteId` they need to pass to `link`.

Terminal window

```bash
npx datocms projects:list
```

###### `cma:call`

Make a single Content Management API call from the terminal, without setting up a Node project. Useful for one-off operations and shell pipelines; for non-trivial work, a typed `@datocms/cma-client-node` import in your editor is usually nicer.

Terminal window

```bash
npx datocms cma:call items create --data='{ "item_type": { "type": "item_type", "id": "..." }, "title": "Hello" }'
```

###### `cma:script`

Run a one-off TypeScript script against the Content Management API. It runs in two modes:

-   **File-mode**: pass a `.ts` file path. The script must export a default async function `(client: Client) => Promise<void>`. Imports resolve against your project's `node_modules`.
-   **Stdin-mode**: pipe top-level-`await` code. The CMA `client` is available as an ambient global, and `Schema.*` types are generated on demand. Named exports of `@datocms/cma-client-node`, `datocms-structured-text-utils`, and `datocms-structured-text-dastdown` are also exposed as globals, so most one-liners need no imports.
    

Terminal window

```bash
npx datocms cma:script <<'EOF'
console.log(
  (await client.items.list({ filter: { type: 'article' } })).map((i) => i.id),
);
EOF
```

Both modes block explicit `any` / `unknown` annotations, casts to `never`, and `@ts-ignore` / `@ts-expect-error` / `@ts-nocheck` directives. Use `console.log()` for output: stdout is piped through cleanly so the command composes with `| jq` and similar.

Even outside an agentic context, `cma:script` is the fastest way to run a one-off script against a project: you get authentication and project-specific types out of the box, with nothing to set up. The file-mode shape is identical to the one produced by `migrations:new`, so a script that turns out to be worth keeping can be moved into the migrations directory and replayed as a migration without rewriting it.

###### `environments:primary`

Print the name of the project's primary environment. Mostly useful inside shell scripts and CI pipelines that need to know which environment to target programmatically; a human developer typically already knows this.

Terminal window

```bash
npx datocms environments:primary
```

## Why these commands exist

A coding agent operating in your editor needs to **discover, inspect, and operate** on a DatoCMS project without going through a browser. Each of the commands above corresponds to one of those steps:

-   `schema:inspect`, `cma:docs`, and `projects:list` for **discovery**: figure out what the project contains, what the API can do, and which projects the account has access to.
-   `cma:call` and `cma:script` for **operation**: make changes through the CMA without scaffolding a Node project around them.
    
-   `environments:primary` for **orientation**: answer "which environment should I work on?" programmatically.
    

Combined with the rest of the CLI (auth, link, environments, migrations, schema generation), an agent has everything it needs to interact agentically with your DatoCMS project. The Agent Skills package the conventions and best practices that turn this surface into a productive workflow.

For the full list of flags accepted by each command above, see the [`datocms` package README](https://github.com/datocms/cli/tree/main/packages/cli) on GitHub or run `datocms <command> --help`.

---

# DatoCMS CLI — Authenticate with an API token

Source [docs]: https://www.datocms.com/docs/cli/authenticate-with-api-token.md

Use an [API token](/docs/content-management-api/authentication.md) to authenticate the CLI when OAuth isn't an option, typically in CI/CD pipelines where there is no browser. The CLI resolves authentication in this order:

1.  `--api-token` flag on the command
    
2.  linked project via OAuth (when `datocms.config.json` is present and you are logged in)
    
3.  environment variable (`DATOCMS_API_TOKEN` for the default profile).
    

The API token must have access to the Content Management API (`can_access_cma: true`). The exact set of role permissions you need depends on what you do with the CLI: schema migrations need permissions on models/fields, environment commands need environment management, maintenance mode needs the maintenance toggle, and so on.

You can pass the token as a parameter to every command:

Terminal window

```bash
$ npx datocms migrations:run --api-token=<YOUR-API-TOKEN> [...]
```

Or expose it as an environment variable:

Terminal window

```bash
$ export DATOCMS_API_TOKEN=<YOUR-API-TOKEN>
$ npx datocms migrations:run [...]
```

The CLI also loads environment variables from a `.env` or `.env.local` file, so you can place the token there. Make sure not to commit the file to your repo:

Terminal window

```bash
$ echo '.env' >> .gitignore
$ echo 'DATOCMS_API_TOKEN=<YOUR-API-TOKEN>' >> .env
```

---

# DatoCMS CLI — Profiles and multi-project setup

Source [docs]: https://www.datocms.com/docs/cli/profiles-and-multi-project-setup.md

A **profile** is a named configuration block inside `datocms.config.json` that points the CLI at a specific DatoCMS project. When your repo only talks to one project, the `default` profile (created automatically by `datocms link`) is all you need. When your repo talks to **multiple** DatoCMS projects (for example, a blueprint project and several client projects), you create additional profiles, and select which one each command operates on.

> [!NOTE] Profiles vs environments
> Profiles are about separating **different projects** or **authentication contexts**, not different environments of the same project. To target a sandbox environment of an already-selected project, use the `--environment` flag instead.

## Creating an additional profile

Run `link` again, passing a profile name:

Terminal window

```bash
npx datocms link --profile=client_a
```

This appends a new profile block to `datocms.config.json`:

```json5
{
  "profiles": {
    "default": {
      "siteId": "12345",
      "organizationId": "67890",
      // ...
    },
    "client_a": {
      "siteId": "99999",
      "organizationId": "88888",
      // ...
    }
  }
}
```

## Selecting the active profile

When you run a command, the CLI picks the active profile in this order:

1.  `--profile=<id>` flag on the command
    
2.  `DATOCMS_PROFILE=<id>` environment variable
    
3.  `default` profile in `datocms.config.json`
    

So these two commands are equivalent:

Terminal window

```bash
npx datocms migrations:run --profile=client_a
DATOCMS_PROFILE=client_a npx datocms migrations:run
```

## Token resolution per profile

When the CLI needs an API token for the active profile, it looks for one in this order:

1.  `--api-token` flag on the command
    
2.  The linked project's OAuth credentials (for profiles created via `datocms login` + `datocms link`)
    
3.  An environment variable, named after the profile: - the `default` profile reads from `DATOCMS_API_TOKEN` - a `client_a` profile reads from `DATOCMS_CLIENT_A_PROFILE_API_TOKEN` - any other profile follows the same `DATOCMS_<PROFILE>_PROFILE_API_TOKEN` convention
    

If you want a custom environment variable name for a profile (for example because your CI provider already has a token under a different name), set the `apiTokenEnvName` property on the profile:

```json5
{
  "profiles": {
    "client_a": {
      "siteId": "99999",
      "apiTokenEnvName": "CLIENT_A_TOKEN"
    }
  }
}
```

> [!WARNING] CDA tokens don't work with the CLI
> The CLI talks to the Content Management API, so the token must have CMA access (`can_access_cma: true`). Read-only CDA tokens like `DATOCMS_READONLY_API_TOKEN` or `NEXT_PUBLIC_DATOCMS_API_TOKEN` won't work.

## Other profile-level configuration

Beyond `siteId` and `apiTokenEnvName`, every profile accepts a few additional properties:

-   `organizationId`: the DatoCMS organization the project belongs to
-   `logLevel`: `NONE`, `BASIC`, `BODY`, or `BODY_AND_HEADERS` (verbosity of CLI output)
    
-   `logMode`: `stdout`, `file`, or `directory`
-   `baseUrl`: for self-hosted or staging DatoCMS instances
    
-   `migrations.directory`: path where migration scripts live
-   `migrations.modelApiKey`: the API key of the DatoCMS model used to track applied migrations
    
-   `migrations.template`: path to a custom migration template file
-   `migrations.tsconfig`: path to a custom `tsconfig.json` for migrations
    

## Use case: keeping multiple projects in sync

A common reason to use profiles is the **blueprint → client** workflow: you maintain a "blueprint" DatoCMS project where you evolve the schema, and you propagate the same migrations to multiple client projects. See [Keeping multiple DatoCMS projects in sync](/docs/scripting-migrations/keeping-multiple-datocms-projects-in-sync.md) for the full workflow.

## Removing a profile

To remove a profile from `datocms.config.json`:

Terminal window

```bash
npx datocms unlink --profile=client_a
```

---

# Environments and migrations — Introduction to environments

Source [docs]: https://www.datocms.com/docs/scripting-migrations/introduction.md

Traditional CMSs often treat content as a one-off effort, which makes content management difficult to fit into existing development lifecycles.

Content environments make it easier for your development team to **manage and maintain the content structure once your content has been published**. Think of environments as code branches: they're great for testing, development and pre-production.

In short, environments ensure quick turnaround times and flexibility for developers — without interrupting the editorial workflow.

### What's an environment?

By default, every project has one environment, called the **primary environment**, which is meant to be used for the regular editorial workflow. Additionally, developers can create multiple **sandbox environments** to safely test and experiment with changes in the content.

(Image content)

Sandbox environments start out as **exact copies of one of the existing environments** (i.e., the primary one). The process of creating a new sandbox from an existing environment is called **forking**.

Each environment is identified by a name (e.g., `master`) and stores the following information:

-   Models
-   Records
    
-   Uploads
-   Plugins
    
-   The content navigation bar
-   Configuration (locales, timezone settings, appearance, SEO preferences)
    

When making changes to any of the aforementioned entities in any environment, including the primary environment, **the data in all other environments remains unaffected**.

### Creating a new sandbox environment

To manage all your project's environments, head over to the *Project Settings \> Environments* section. To create a new sandbox starting from an existing environment, click on the contextual menu \> **Fork**, and choose a name for the new environment.

(Video content)

DatoCMS will perform a deep copy of all the information contained inside the source and transfer it to the new sandbox.

Once there's at least one sandbox environment, developers will be able to **switch environments using the top bar panel**.

Editors will never see this panel due to a reduced set of permissions and will continue their editorial workflow in the primary environment as usual.

(Video content)

### Promotion of sandbox environments

At any time, you can **promote a sandbox environment to become the new primary environment**. The old primary environment will be demoted to a sandbox environment, and content editors will immediately see the interface refresh. From that moment, they will only be able to see and make changes to the new primary environment.

To be updated when a sandbox gets promoted, you can [set up a webhook](/docs/general-concepts/webhooks.md#webhook-triggers) listening to the "Environment Promote" event.

### Renaming environments

At any time, you can change the name of an existing environment. This change won't impact those working on the CMS:

(Video content)

To be updated when a sandbox gets renamed, you can [set up a webhook](/docs/general-concepts/webhooks.md#webhook-triggers) listening to the "Environment Update" event.

### Forcing use of sandbox environments

Changes to a primary environment can be potentially disruptive, so we give you the ability to **block any user from editing the primary schema or configuration.**

You can do this by going to Project settings \> Global properties and enabling "**Force the use of sandbox environments".** If enabled, no user can edit the primary environment and make changes to its schema and configuration, regardless of their role.

---

# Environments and migrations — Safe iterations using environments

Source [docs]: https://www.datocms.com/docs/scripting-migrations/safe-iterations-using-environments.md

## What not do do

A developer who needs to work on a new feature **should never make direct changes to the schema (models/fields/etc.) in the primary environment**.

There are multiple reasons for this:

-   the changes might **severely interfere with the work of the editors** working on the live website;
-   the changes might modify the format of some API call responses that are required by the live website, and **break the experience for end users**;
    
-   the changes could be accidentally wrong, and **produce significant loss of data**.
    

## Safely working on a change to the content schema

Using environments, you can instead follow this safe workflow:

1.  **Create a new sandbox environment,** forking the primary one. From now on, exclusively work inside the sandbox — this will safeguard you from all the problems just mentioned above!
    
2.  Create a branch in the Git repositoryof your website/app, and inside it, start **reading content from the sandbox environment** instead of the primary environment;
    
3.  **Manually write a migration script (or** [**auto-generate it**](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md#option-2-autogenerate-a-migration-script)**).** As we'll cover thoroughly in the next sections, a [migration script](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md) is just a sequence of API calls to the [Content Management API](/docs/content-management-api.md) that produces changes to the schema of an environment;
    
4.  **Run the migration script** on the sandbox environment to apply the changes;
    
5.  **Adapt the code of your website/app** to the changes.
    

If you make any changes to the migration script after running it, you can re-test it by simply repeating steps 1, 3, and 4.

> [!POSITIVE] Environments enable teamwork!
> Just like Git branches, environments let multiple development teams work simultaneously on different changes to the content schema, without interfering with each other. Everyone has their own copy of the schema, and can test/iterate freely.

### Why migration scripts are important?

As long as you are working in a sandbox environment, you may not even need a migration script, you can just make your changes to models/fields using the interface... but eventually the time will come to merge your work and changes into production.

The primary environment at this point, however, may have changed significantly; we can't just promote our sandbox environment to primary, because it is stuck at a past snapshot of the primary environment made at the time of the fork, and we would lose all the work done in the primary environment from that point forward.

Having an explicit migration script **makes your changes reproducible**. It allows the *exact same steps* tested in the sandbox environment to be performed on the primary environment itself!

Let's see exactly *how* in the next section.

> [!POSITIVE] Migration scripts can be auto-generated!
> By writing migration scripts by hand, you lose one of the core strengths of DatoCMS: the convenience of a graphical interface for editing your content schema.
> 
> Fortunately, you can also [auto-generate migration scripts](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md#option-2-autogenerate-a-migration-script)! In this case, you can make the necessary changes to the schema via the UI, and get back a migration script with a sequence of API calls that produce the same results.

## Safely merging a change to the content schema

Once everything is ready to be shipped in production, follow this process — of course, it can be adapted to your specific deployment workflow:

1.  **Turn on maintenance mode** (covered in detail in [Apply migrations to primary environment](/docs/scripting-migrations/apply-migrations-to-primary-environment.md)), so that during the deployment process no one can write new data on the primary environment. Before enabling maintenance mode, DatoCMS will warn you if other collaborators are currently working on some content, so you can decide to postpone the deployment and contact the editors.
    
2.  **Merge the Git feature branch** containing the adaptation of your website/app code to the new changes;
    
3.  **Fork the primary environment into a new sandbox environment**, and re-run the migration script on it;
    
4.  **Promote the sandbox environment to be the new primary**. The old primary environment will in turn become a sandbox, ready to be promoted again **as an instant rollback** in case of errors you might find later on in production. We put no expiration dates on sandbox environments, which means that development teams can potentially create multiple restore points;
    
5.  **Deploy your website/app** with the merged changes;
    
6.  **Test that everything works** on your live website/app;
    
7.  **Turn off maintenance mode** to allow content editors to get back to their regular work on the new primary.
    

To learn more, visit the [Apply migrations to primary environment](/docs/scripting-migrations/apply-migrations-to-primary-environment.md) guide.

## Using sandbox environments on CI/automated testing

If the team relies heavily on automated testing, **environments can be created programmatically and just for the duration of a test**. Once it has successfully passed, the environment can be programmatically deleted.

Environments enable continuous integration by allowing you to create a “template environment” to use during tests. This template maintains the exact state you need to run your tests. Because environments are meant to be used as temporary entities for isolation, you don’t need to run any clean-up tasks.

Instead, just delete and recreate a new environment for every test.

---

# Environments and migrations — Write and test migration scripts

Source [docs]: https://www.datocms.com/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md

## Creating your first migration script

For this example, we're starting with a blank DatoCMS project, and then progressively add models/records using migrations.

There are two ways to create a migration script:

1.  By writing it manually, or
    
2.  By having the CLI automatically generate it for you.
    

We'll cover both methods in detail below.

> [!WARNING] Leverage TypeScript to simplify your work!
> Since our Content Management API client is fully typed, we strongly suggest writing your migration scripts in TypeScript. You will get auto-completion suggestions on every call to an endpoint, and type checks for free.
> 
> If your project has a `tsconfig.json` file, the `datocms migrations:new` command will automatically create migration scripts in TypeScript, but you can also manually pass the `--ts` flag to the `migrations:new` command.

### Option 1: Write a migration script manually

Let's create an *Article* model with a simple *Title* field. With the [CLI tool successfully set up](/docs/cli.md), run the following command inside your project:

Terminal window

```bash
$ npx datocms migrations:new 'create article model'

Created migrations/1591173668_createArticleModel.js
```

This will create a script inside your `migrations` directory named `<TIMESTAMP>_createArticleModel.js`.

Let's take a look at its content:

```javascript
'use strict';

/** @param client { import("datocms/lib/cma-client-node").Client } */
module.exports = async (client) => {
  // DatoCMS migration script

  // For more examples, head to our Content Management API docs:
  // https://www.datocms.com/docs/content-management-api

  // Create an Article model:
  // https://www.datocms.com/docs/content-management-api/resources/item-type/create

  const articleModel = await client.itemTypes.create({
    name: 'Article',
    api_key: 'article',
  });

  // Create a Title field (required):
  // https://www.datocms.com/docs/content-management-api/resources/field/create

  const titleField = await client.fields.create(articleModel, {
    label: 'Title',
    api_key: 'title',
    field_type: 'string',
    validators: {
      required: {},
    },
  });

  // Create an Article record:
  // https://www.datocms.com/docs/content-management-api/resources/item/create

  const article = await client.items.create({
    item_type: articleModel,
    title: 'My first article!',
  });
}
```

The script exports an async function with a `client` argument, which is an instance of our [Content Management API client](/docs/content-management-api/using-the-nodejs-clients.md#initialize-the-client).

Incidentally, the body of the function is already filled with everything we need for this particular example, but of course, you can rewrite the migration script to your liking using any method available in our [Content Management API](/docs/content-management-api.md) to produce the desired results.

> [!POSITIVE] Custom migration script template?
> If you would like to scaffold new migration scripts from a custom template instead of the default one, feel free to pass the `--template` flag. Or, even better, you can add it as a default setting to your profile with the `datocms link` command, so that the choice will propagate to every other team member.

#### Running the migration script

To execute the migration, run the following command:

Terminal window

```bash
$ npx datocms migrations:run --destination=feature-branch
```

Here's the result:

(Video content)

Upon execution, the command does the following:

-   Forks the primary environment into a new sandbox environment called `feature-branch`;
-   Runs any pending migrations inside the sandbox environment.
    

> [!POSITIVE] How the CLI keeps track of already-run migrations?
> To track which migrations have already been run in a specific environment, the CLI creates a special `schema_migration` model in your project. After each migration script completes, it creates a record referencing the name of the script itself.
> 
> You can configure the name of the model with the `--migrations-model` flag, or configure your profile accordingly with the `datocms link` command!

To verify that only pending migrations are executed, we can re-run the same command and see the result:

```plaintext
$ npx datocms migrations:run --destination=feature-branch

Migrations will be run in "feature-branch" sandbox environment

Creating a fork of "main" environment called "feature-branch"... !
 ›   Error: Environment "feature-branch" already exists!
 ›   Try this:
 ›     * To execute the migrations inside the existing environment, run "datocms migrations:run --source=feature-branch --in-place"
 ›     * To delete the environment, run "datocms environments:destroy feature-branch"
```

Ouch! The sandbox environment `feature-branch` already exists, so the command failed. We can follow the CLI suggestion to re-run the migrations inside the already existing sandbox environment:

Terminal window

```bash
$ npx datocms migrations:run --source=feature-branch --in-place

Migrations will be run in "feature-branch" sandbox environment

No new migration scripts to run, skipping operation
```

As you can see, no migration gets executed, as our script has already been run in this environment!

> [!POSITIVE] Programmatically parse the CLI output
> Remember that you can always add the `--json` flag to any CLI command, to get a JSON output, easily parsable by tools like [`jq`.](https://stedolan.github.io/jq/)

### Option 2: Autogenerate a migration script

Let's create a new migration script to add a new *Author* model, and an *Author* field on the article. This time, we're going to use the `--autogenerate` flag on the `migrations:new` command.

The `--autogenerate` flag takes two environment names as arguments:

```plaintext
$ npx datocms migrations:new --help

[...]

--autogenerate=<value>
    Auto-generates script by diffing the schema of two environments

    Examples:
    * --autogenerate=foo finds changes made to sandbox environment 'foo' and
    applies them to primary environment
    * --autogenerate=foo:bar finds changes made to environment 'foo' and applies
    them to environment 'bar'
```

When using `--autogenerate`, the API token must have permissions to read schema data from both environments being compared. At minimum, the role needs these permissions on both environments:

-   Customize content navigation bar
-   Create/edit models and plugins
    
-   Create/edit workflows
-   Create/edit shared filters
    

So first we need to make a copy of our `feature-branch` environment (let's call it `with-authors`):

Terminal window

```bash
$ npx datocms environments:fork feature-branch with-authors

Creating a fork of "feature-branch" called "with-authors"... done
```

Then add our Author and Author field to the `with-authors` environment using the regular DatoCMS interface, and then run the following command to generate a migration script:

Terminal window

```bash
$ npx datocms migrations:new addAuthors --autogenerate=with-authors:feature-branch

Writing "migrations/1653062813_addAuthors.js"... done
```

Let's see the result:

```javascript
/** @param client { import("datocms/lib/cma-client-node").Client } */
module.exports = async function (client) {
  const newFields = {};
  const newItemTypes = {};
  const newMenuItems = {};

  console.log('Create new models/block models');

  console.log('Create model "Author" (`author`)');
  newItemTypes['531556'] = await client.itemTypes.create(
    {
      name: 'Author',
      api_key: 'author',
      all_locales_required: true,
      collection_appearance: 'table',
    },
    { skip_menu_item_creation: 'true' },
  );

  console.log('Creating new fields/fieldsets');

  console.log('Create Single-line string field "Name" (`name`) in model "Author" (`author`)');
  newFields['2791804'] = await client.fields.create(newItemTypes['531556'], {
    label: 'Name',
    field_type: 'string',
    api_key: 'name',
    validators: { required: {} },
    appearance: {
      addons: [],
      editor: 'single_line',
      parameters: { heading: true },
      type: 'title',
    },
    default_value: '',
  });

  console.log('Create Single link field "Author" (`author`) in model "Blog Post" (`blog_post`)');
  newFields['2791806'] = await client.fields.create('810907', {
    label: 'Author',
    field_type: 'link',
    api_key: 'author',
    validators: {
      item_item_type: {
        on_publish_with_unpublished_references_strategy: 'fail',
        on_reference_unpublish_strategy: 'delete_references',
        on_reference_delete_strategy: 'delete_references',
        item_types: [newItemTypes['531556'].id],
      },
      required: {},
    },
    appearance: { addons: [], editor: 'link_select', parameters: {} },
  });

  console.log('Finalize models/block models');

  console.log('Update model "Author" (`author`)');
  await client.itemTypes.update(newItemTypes['531556'], {
    title_field: newFields['2791804'],
  });

  console.log('Manage menu items');

  console.log('Create menu item "Authors"');
  newMenuItems['265140'] = await client.menuItems.create({
    label: 'Authors',
    item_type: newItemTypes['531556'],
  });
};
```

Wow! Thanks CLI, that's a lot of code for free! 😀

If we run the migrations again in our `feature-branch` environment, we can verify that the script is indeed working:

Terminal window

```bash
$ npx datocms migrations:run --source=feature-branch --in-place

Migrations will be run in "feature-branch" sandbox environment

Running migration "1653062813_addAuthors.js"...
Create new models/block models
Create model "Author" (`author`)
Creating new fields/fieldsets
Create Single-line string field "Name" (`name`) in model "Author" (`author`)
Finalize models/block models
Update model "Author" (`author`)
Manage menu items
Create menu item "Authors"
done
```

> [!WARNING] Automigrations are only for schema changes!
> The `--autogenerate` flag will not take into account changes made to records and uploads! If you need those, you are required to write your own migration script manually — or extend the one that the autogeneration tool created for you.

## Re-applying a migration script

Suppose that you need to make a change to a migration script after running it.

Since we're working on a sandbox, to test the new script, we can simply delete the current sandbox, fork a new one from the primary environment and re-run the migrations again:

Terminal window

```bash
$ npx datocms environments:destroy feature-branch

Destroying environment "feature-branch"... done

$ npx datocms migrations:run --destination=feature-branch

Migrations will be run in "feature-branch" sandbox environment

Creating a fork of "main" environment called "feature-branch"... done
Creating "schema_migration" model... done
Running migration "1653061497_createArticleModel.js"... done
Running migration "1653062813_addAuthors.js"... done
Successfully run 2 migration scripts

Done!
```

# Adapt your website/app to the schema changes

It goes without saying that you also need to work on your website/app to adapt it to the changes you just made. We suggest working on a Git feature branch, and storing the `migrations` directory in the same repo as your frontend code.

All of our APIs and integrations offer a way to point to a sandbox environment instead of the primary one.

For example, if you're using our [GraphQL Content Delivery API](/docs/content-delivery-api.md), you can explicitly [read data from a specific environment](/docs/content-delivery-api/api-endpoints.md#specifying-an-environment) using one of the following endpoints:

```plaintext
https://graphql.datocms.com/environments/{ENVIRONMENT-NAME}
https://graphql.datocms.com/environments/{ENVIRONMENT-NAME}/preview
```

Once everything is working as expected, we can [ship everything to production](/docs/scripting-migrations/apply-migrations-to-primary-environment.md).

## Deleting a sandbox environment

After you've run your tests you might need to programmatically delete a sandbox environment.

In this case you can simply run:

Terminal window

```bash
$ npx datocms environments:destroy <SANDBOX-ENVIRONMENT-NAME>
```

## Using Fast Fork option for large environments

When working with large environments, a fork process can become slow. To address this issue, DatoCMS offers a "fast fork" option that can be up to 20 times faster than a regular fork. However, it's important to note that during the fork process, the source environment will be kept in read-only mode, which means that other users won't be able to make any changes to its content. This is similar to turning on maintenance mode (covered in [Apply migrations to primary environment](/docs/scripting-migrations/apply-migrations-to-primary-environment.md)).

To use the fast fork option, you can select it either from the DatoCMS interface or the CLI.

To run a fast fork on the DatoCMS interface, simply select the "Perform a fast fork?" option when creating a fork:

(Video content)

On the CLI, both the `migrations:run` and the `environments:fork` commands support an additional flag for the fast fork option.

To use the fast fork option with the `migrations:run` command, you can run the following command:

Terminal window

```bash
$ npx datocms migrations:run --destination=new-sandbox-env --fast-fork
```

Similarly, to use the fast fork option with the `environments:fork` command, you can run the following command:

Terminal window

```bash
$ npx datocms environments:fork source-env destination-env --fast
```

###### Forcing a fast fork

The `--force` option is used in the CLI to force the start of a fast fork process in any case, even if a user is currently making changes to a record.

To use the `--force` option, you can add it to the end of the command like this:

```plaintext
$ npx datocms environments:fork source-env destination-env --fast --force
$ npx datocms migrations:run --destination=new-sandbox-env --fast-fork --force
```

By adding the `--force` option to the end of the command, you're telling the CLI to proceed with the fast fork process even if there are users making changes to records.

It's important to use the `--force` option with caution, as it can potentially destroy in-progress work made by other users. It's recommended to communicate with other users and coordinate with them before using the `--force` option to avoid any issue.

### Learn more about migrations

Check out this tutorial on how to migrate your content schema using scripts:

[

(Image content)

Using scripts to migrate DatoCMS content schema

Play video »

](https://youtu.be/AzeUU7bjDco)

---

# Environments and migrations — Apply migrations to primary environment

Source [docs]: https://www.datocms.com/docs/scripting-migrations/apply-migrations-to-primary-environment.md

Once we're done [writing and testing our migrations](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md), we need to apply them to the primary environment.

The first solution that comes to mind would be to simply promote the sandbox environment we were using to test our migrations to primary, but this can be a very dangerous operation, as **the primary environment might have diverged from your sandbox environment**. That is, some users or webhooks you've set up might have made changes to either the content or the schema of the primary environment after the fork.

This is instead the safe workflow that we suggest for applying migrations to the primary environment.

### Step 1: Turn on maintenance mode to prevent changes to the primary environment

The first thing to do is turn on maintenance mode, so that **during the process, no one can write new data to the primary environment**.

You can do so either from the DatoCMS interface:

(Video content)

Or using the CLI:

Terminal window

```bash
$ npx datocms maintenance:on

Activating maintenance mode... done
```

If some users are in the process of editing any record when you launch the command, DatoCMS will warn you and fail the execution of the command. You can force the activation using the `--force` flag:

Terminal window

```bash
$ npx datocms maintenance:on

Activating maintenance mode... !
 ›   Error: Cannot activate maintenance mode as some users are currently editing records
 ›   Try this: To proceed anyway, use the --force flag
```

### Step 2: Run the migrations on a newly forked sandbox environment

You can now call the `datocms migrations:run` command to make a new copy of the primary environment, and run all the pending migrations:

Terminal window

```bash
$ npx datocms migrations:run --destination=new-main --fast-fork

Migrations will be run in "new-main" sandbox environment

Creating a fork of "main" environment called "new-main"... done
Creating "schema_migration" model... done
Running migration "1653061497_createArticleModel.js"... done
Running migration "1653062813_addAuthors.js"... done

Successfully run 2 migration scripts
```

Notice that, since we're already in Maintenance Mode, we can safely use the `--fast-fork` flag to [run a fast fork](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md#using-fast-fork-option-for-large-environments), which can be up to 20x faster than the regular fork.

### Step 3: Test your apps pointing them to the new sandbox

Before promoting the new sandbox as primary, **make sure that your website/app is working correctly**. All of our integrations offer a way to point to a sandbox instead of the primary environment.

As an example, if you're using our GraphQL Content Delivery API, you can explicitly [read data from a specific environment](/docs/content-delivery-api/api-endpoints.md#specifying-an-environment) using one of the following endpoints instead of the regular ones:

```plaintext
https://graphql.datocms.com/environments/{ENVIRONMENT-NAME}
https://graphql.datocms.com/environments/{ENVIRONMENT-NAME}/preview
```

### Step 4: Promote the sandbox and turn off maintenance mode

Once everything is ready, you can safely promote the sandbox to be the new primary environment. The old primary environment will be demoted to sandbox, and content editors will immediately see a refresh in the interface. From that moment, they will only be able to see and make changes to the new primary environment.

From the interface:

(Video content)

You can also promote an environment to primary via CLI:

Terminal window

```bash
$ npx datocms environments:promote <SANDBOX-ENVIRONMENT-NAME>
```

When you're ready, you can turn off maintenance mode to allow content editors to return to their regular editorial workflow:

Terminal window

```bash
$ npx datocms maintenance:off

Deactivating maintenance mode... done
```

## Handling rollbacks

In the unfortunate event you deploy some bad code, you can rollback to a prior, known good version of your project simply **re-promoting your old primary environment** and re-deploying your frontend/apps to the previous state. The effect is immediate, no re-compilation is required.

#### Learn more about data migrations

Check out this tutorial on how to migrate your content schema using scripts:

[

(Image content)

Using scripts to migrate DatoCMS content schema

Play video »

](https://youtu.be/AzeUU7bjDco)

---

# Environments and migrations — Running legacy migration scripts

Source [docs]: https://www.datocms.com/docs/scripting-migrations/running-legacy-migrations.md

If you have migration scripts written for the old `datocms-client` package, you don't need to convert them to the latest `@datocms/cma-client` package.

You can simply move them to a `legacyClient` directory:

Terminal window

```bash
mkdir migrations/legacyClient
mv migrations/*.js migrations/legacyClient
```

The CLI will take care of passing those migration scripts to the old API client, while all the new migration scripts will be written directly in the `migrations` directory, and will use the new API client.

---

# Environments and migrations — Keeping multiple DatoCMS projects in sync

Source [docs]: https://www.datocms.com/docs/scripting-migrations/keeping-multiple-datocms-projects-in-sync.md

If you're an agency or developer looking to streamline your development process, this guide will provide you with the necessary steps to efficiently manage multiple projects within DatoCMS.

### Why is it useful?

Managing multiple projects can be time-consuming and error-prone, especially when each project requires similar configurations and updates. However, with the ability to create a blueprint project in DatoCMS and duplicate it for each client, you can significantly reduce development time and ensure consistency across your projects.

By following the techniques outlined in this guide, you will be able to:

1.  **Save time and effort:** Rather than starting each project from scratch, you can create a blueprint project with all the necessary configurations, models, and settings. This allows you to duplicate and then customize the blueprint for each client, minimizing the time spent on repetitive tasks.
    
2.  **Ensure consistency:** Keeping multiple projects in sync ensures that any updates or improvements made to the blueprint project can be easily propagated to all the client projects. This guarantees consistency in design, functionality, and content management across your portfolio.
    
3.  **Maintain scalability:** As your agency grows and takes on more clients, the ability to efficiently manage multiple projects becomes crucial. By adopting a synchronized approach, you can handle a higher workload without sacrificing quality or increasing development time.
    

In the following sections, we will explore various use cases and provide step-by-step instructions on how to keep your DatoCMS projects in sync. Let's dive in and discover how you can optimize your development workflow!

### Creating a blueprint project

Creating a blueprint project in DatoCMS is a great way to streamline your development process:

1.  Start by setting up a new project in DatoCMS that will serve as your blueprint. Configure it with all the necessary models, fields, plugins, and settings that you want to replicate across your client projects.
    
2.  In parallel to your DatoCMS project, also create a frontend project associated with the blueprint. Use your favorite technology (ie. Next, SvelteKit). Make sure to parameterize the DatoCMS API token using environment variables!
    
3.  Once your blueprint project is ready, make sure to thoroughly test it and ensure that everything is working as expected.
    

### Duplicating your blueprint project

Now that you have your blueprint project set up, it's time to duplicate it for each client project.

Open your DatoCMS dashboard, enter the blueprint project, and under the "Danger zone" section click on "Duplicate project". Make sure to check the "Duplicate only models and fields" option, so that any sample content present in the blueprint won't be copied.

> [!POSITIVE] Different projects, same IDs
> By duplicating a project, DatoCMS keeps exactly the same IDs for all the entities. As we'll see in the next section, this is vital to keep your projects synced over time.

Open your Netlify/Vercel account, and create a new project, pointing to the Git repo of your blueprint frontend. In the project settings, make sure to specify the API token of the cloned project as an environment variable.

You can repeat these steps for each client.

### Propagating updates across client projects

Keeping your projects in sync is crucial for maintaining consistency and efficiency. Using migrations, you can easily make changes to your blueprint project, and propagate them to every client project programmatically.

##### Step 1: Setup the CLI

In your blueprint frontend repo, install our CLI:

Terminal window

```bash
npm install --save-dev datocms
```

First, log in to your DatoCMS account if you haven't already:

Terminal window

```bash
npx datocms login
```

Then, link a profile called `blueprint` to your blueprint DatoCMS project:

Terminal window

```bash
npx datocms link --profile=blueprint --migrations-dir=migrations
```

This will create a `datocms.config.json` file. Similarly, link one profile for each of your client projects:

Terminal window

```bash
npx datocms link --profile=<client_name> --migrations-dir=migrations
```

Each profile will be linked to a specific DatoCMS project via OAuth, so every CLI command automatically resolves the correct API token using your personal credentials. No environment variables needed for local development.

> [!POSITIVE] Using API tokens in CI/CD?
> If OAuth login isn't practical (e.g. CI/CD pipelines), you can still provide API tokens via environment variables. During the link process, you can configure a custom environment variable name per profile using the apiTokenEnvName option in datocms.config.json.

##### Step 2: Generate a migration script

Follow the [Write and test migration scripts](/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md) guide to generate a migration with the changes you want to make to every project. Make sure to add the `--profile=blueprint` flag to every command you launch, so that in this phase you will always work on your blueprint project.

##### Step 3: Run the migration in every project

Now that we have a migration script, we can easily apply the same set of changes to every project by following the steps listed in the [Apply migrations to primary environment](/docs/scripting-migrations/apply-migrations-to-primary-environment.md) guide.

Here is a sample Bash script that you can use to automate the process:

```bash
#!/bin/bash

# Put the names of the profiles associated to your DatoCMS projects here:
profiles=("blueprint" "foo" "bar" "qux")

# Get current date
current_date=$(date +%Y-%m-%d)

# Generate the name of the new primary environment, using current date
destination="new-main-$current_date"

# Iterate over each profile
for profile in "${profiles[@]}"
do
    echo "Running commands for profile: $profile"

    # Enable maintenance mode
    npx datocms maintenance:on --force --profile="$profile"

    # Fork primary environment, run migrations in the new destination
    npx datocms migrations:run --destination="$destination" --fast-fork --profile="$profile"

    # Promote environment
    npx datocms environments:promote "$destination" --profile="$profile"

    # Disable maintenance mode
    npx datocms maintenance:off --profile="$profile"

    echo "Commands completed for profile: $profile"
done
```

---

# Import and Export — Available Export & Backup Options

Source [docs]: https://www.datocms.com/docs/import-and-export/export-data.md

At DatoCMS, ensuring the security and integrity of your data is our top priority. Our [**ISO 27001**](https://www.datocms.com/blog/iso-27001.md) **certification** guarantees that our architecture is designed with internal backups, providing a reliable safeguard against data loss. In other words, you can rest assured that we follow best practices to keep your data safe.

However, data security is a complex and multifaceted issue. That’s why having a **clear, precise, and reliable plan** is essential to protect against potential risks, including human errors at any level.

To help you navigate this, we’ve broken down different approaches depending on your specific needs:

### **You trust DatoCMS, but need backup solutions to recover from human errors on your end**

For this scenario, DatoCMS provides a powerful feature: [**environments**](/docs/scripting-migrations/introduction.md). Environments allow you to create **complete copies (forks) of your project’s data**. These copies are separate sandboxes that can be promoted to replace your primary environment in case of accidental data loss.

Additionally, plugins like [(Image content)Automatic environment backups](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-automatic-environment-backups.md)are available that let you **automate periodic backups** (forks) of your primary environment, ensuring you always have a rolling backup in place.

Another common cause of human error is unintentionally deleting records, and another plugin, [(Image content)Record bin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-record-bin.md), can assist you in managing that as well.

### **You want to ensure protection against potential data loss on DatoCMS’s end**

While our infrastructure is designed to prevent data loss, we understand that you may still want an extra layer of protection.

The first key assurance is that all content within DatoCMS is **accessible through APIs**, allowing you to generate **offline backups** and store them outside our architecture.

To facilitate this, you have multiple options:

-   **Use ready-made plugins** from the DatoCMS Marketplace designed for manually exporting your data or managing offline backups/restore functionality, like [(Image content)Project Exporter](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-project-exporter.md) or [(Image content)Export To Google Docs](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-export-to-google-docs.md)
-   **Write a script using our REST API** ( [Content Management API Overview](/docs/content-management-api.md) ) to programmatically export your data
    
-   **Enterprise customers** can access a [**periodic export feature**](/docs/import-and-export/datocms-site-export-feature.md) managed by DatoCMS, which automatically exports project data to an external cloud provider storage.
    

### A template for a custom export script

Exporting your DatoCMS data or making offline backups is easy with our [Content Management API](/docs/content-management-api.md). Here's a quick example script that dumps every record into a `records.json` file:

```javascript
import { buildClient } from '@datocms/cma-client-node';
import fs from 'fs/promises';

async function main() {
  const client = buildClient({
    apiToken: 'YOUR-FULL-ACCESS-API-KEY',
    environment: 'YOUR-ENVIRONMENT-NAME',
  });

  const itemTypes = await client.itemTypes.list();
  const models = itemTypes.filter((itemType) => !itemType.modular_block);
  const modelIds = models.map((model) => model.id);

  const records = [];

  for await (const record of client.items.listPagedIterator({
    nested: true,
    filter: { type: modelIds.join(',') },
  })) {
    records.push(record);
  }

  const jsonContent = JSON.stringify(records, null, 2);

  await fs.writeFile('backupProduction.json', jsonContent, 'utf8');
}

main();
```

And here is a simple script that exports all assets, and downloads them locally:

```javascript
import { buildClient } from '@datocms/cma-client-node';
import fetch from 'node-fetch';
import { writeFile } from 'fs/promises';

async function downloadImage(url) {
  const response = await fetch(url);
  const buffer = await response.buffer();
  const fileName = new URL(url).pathname.split('/').pop();
  await writeFile('./' + fileName, buffer);
}

async function main() {
  const client = buildClient({
   apiToken: 'YOUR-FULL-ACCESS-API-KEY',
   environment: 'YOUR-ENVIRONMENT-NAME',
  });

  const site = await client.site.find();

  for await (const upload of client.uploads.listPagedIterator()) {
    const imageUrl = 'https://' + site.imgix_host + upload.path;
    console.log(`Downloading ${imageUrl}...`);
    downloadImage(imageUrl);
  }
}

main();
```

You can then add this script into a cron-job and store the result in a S3 bucket, upload it to another system, or back up the results locally.

## Recap

Here's a structured comparison table summarizing the key aspects of the two backup scenarios:

| Aspect | Using DatoCMS Environments | External Backup Solutions |
| --- | --- | --- |
| Ease of setup | Very easy and quick to implement | More complex; requires external tools or scripts |
| Data storage location | Within DatoCMS infrastructure | Stored outside DatoCMS (e.g., cloud storage, local) |
| Ease of data restoration | Restore is immediate: a single button click or API call | Complex to restore |
| Risk factors | Pointless if you aim to protect against DatoCMS-related failures | Very safe from DatoCMS failures |
| Automation | Possible through the Automatic Environment Backups plugin or periodic API calls | Possible via existing plugins or custom scripts |
| Management complexity | Low — handled entirely within DatoCMS | High — requires managing storage, automation, and restoration |
| Cost | Creating backup environments will raise the overall number of records in your project, possibly incurring extra costs. | Depends on the strategy: custom export scripts can increase the number of API calls per month, while Project Exports is an additional Enterprise feature |
| Best for... | Users who trust DatoCMS but need a safety net for human errors | Users who want full control and protection from DatoCMS-related failures |

---

# Import and Export — Enterprise Project Exports

Source [docs]: https://www.datocms.com/docs/import-and-export/datocms-site-export-feature.md

The **Project Export** feature allows **Enterprise customers** to export all content and assets from their DatoCMS project to their own **AWS S3 bucket**. This export provides a structured snapshot of your data in JSON format, along with all uploaded assets.

> [!POSITIVE] Security and integrity of your data is our top priority!
> Our [ISO 27001 certification](https://www-draft.datocms.com/blog/iso-27001) ensures that our architecture incorporates internal backups, delivering a dependable safeguard against data loss. In other words, you can be confident that we adhere to best practices for keeping your data secure.
> 
> This Enterprise functionality serves as an additional layer of protection to ensure your safety and should be utilized as a last resort.

### Key Points to Consider

-   **Enterprise Only**: This feature is available exclusively to Enterprise customers.
-   **Not a Backup Solution**: The export does not offer a one-click restoration process.
    
-   **Primary Environment Only**: Only the primary project environment is included in the export.
-   **Automated and Scheduled**: Exports occur on a predefined schedule, with a minimum frequency of **once per month** and a maximum of **once per day**.
    
-   **AWS S3 Storage Required**: Customers must configure their own S3 bucket to receive the exported data.
    

## What is Included in the Export?

The exported data includes:

-   **Schema Models**: Fields and fieldsets.
-   **Schema Blocks**: Block definitions and fields.
    
-   **Records**: Current and published versions, including block records.
-   **Uploads**: Metadata and references for uploaded assets.
    
-   **Project Settings**: Locales, SEO settings, workflows, and installed plugins.
-   **Asset Files**: All uploaded files from the media area.
    

### JSON Snapshots vs. Asset Syncing

While JSON snapshots are periodic and remain unchanged once created, assets are simply synced to their latest versions in the same directory every time. This is why **bucket versioning is recommended**—if an asset is removed from the project, it will also be deleted from the bucket. However, with versioning enabled, you can still retrieve previous versions of deleted assets.

## What is NOT Included?

The export **does not** include:

-   Record revision history
-   API tokens, webhooks, and build triggers
    
-   Collaborators, roles, and permissions
-   Audit logs and usage statistics
    
-   SSO settings and user accounts
-   Any additional metadata not explicitly listed
    

## How to enable Project Export

This feature **cannot be enabled by customers directly**. To set up an export, you must [**contact DatoCMS support**](https://www.datocms.com/support.md?topics=business-partnerships%2Fgeneral-requests) and provide the following details:

1.  **S3 Bucket Name**
    
2.  **AWS Region**
    
3.  **S3 Access Key ID**
    
4.  **S3 Secret Access Key**
    
5.  **Maintenance Mode Enabled/Disabled**: Whether you want us to enable [Maintenance Mode](/docs/cli/environment-migration-and-maintenance-commands.md) on your primary environment during the export process, which can take minutes or hours depending on volume. "Enabled" makes the environment read-only, guaranteeing that your export will be internally consistent. "Disabled" leaves the environment writable as normal, but content & schema edits made *during the export process* might not be correctly included— in some cases, this may result in a mismatch between individual records/versions and their edited schema. **For maximum integrity, we recommend scheduling exports for off hours, with Maintenance Mode enabled.**
    
6.  **Schedule**: How often, and what time (UTC) you want the export to run. Minimum frequency is once a week and maximum is once per day. You can tell us in plain language, or as a [crontab schedule](https://crontab.guru/).
    

Additionally, you must configure your AWS S3 bucket with:

-   **Public access blocked** (mandatory for security)
-   **Bucket versioning enabled** (recommended for data recovery)
    
-   **Lifecycle rules** (optional, for automatic cleanup of old snapshots)
    

Our support team will give you detailed instructions on how to setup everything correctly.

## Export structure

Once configured, each export generates a timestamped snapshot in your S3 bucket. The structure is as follows:

```plaintext
assets/
  project_<ID>/
    file1.png
    file2.mp4

content/
  project_<ID>/
    snapshot_<TIMESTAMP>/
      models/
      records/
      uploads/
      workflows/
      site.json
```

The presence of a `canary.txt` file in a snapshot directory confirms that the export was completed successfully.

## JSON files format

JSON files in the snapshots are similar to the JSON content you can fetch from our [Content Management API](/docs/content-management-api.md), with some changes to reduce scattering across multiple files.

### Schema

For each schema model/block model, a file following this path is present in the bucket:

`content/project_<ID>/snapshot_<TIMESTAMP>/models/<api_key>.json`

For instance:

`content/project_999/snapshot_1721033044/models/article.json`

The `data` key contains the `item_type` resource. `Fields` and `fieldsets` are referenced by their IDs, and their full payload is present in the `included` key.

```json5
{
  "data": {
    "id": "UVP2y5QPToWPXqJbMszyFg",
    "type": "item_type",
    "attributes": {
      "api_key": "article",
      "name": "Article",
      // ... the rest of item_type attributes
    },
    "relationships": {
      "fields": {
        "data": [
          { "id": "InMbgf7BSo2TDG4HYGb2Ug", "type": "field" }
        ]
      },
      "fieldsets": {
        "data": [
          { "id": "bwk17lanRYCOvXKztPp5PA", "type": "fieldset" }
        ]
      },
      "workflow": {
        "data": { "id": "MQLtfJv4Q22nKUoHEQ3b9A", "type": "workflow" }
      }
    },
    "meta": { "has_singleton_item": false }
  },
  "included": [
    {
      "id": "InMbgf7BSo2TDG4HYGb2Ug",
      "type": "field",
      "attributes": {
        "label": "Content",
        // ... the rest of field attributes
      },
      "relationships": {
        "item_type": {
          "data": { "id": "UVP2y5QPToWPXqJbMszyFg", "type": "item_type" }
        },
        "fieldset": {
          "data": { "id": "bwk17lanRYCOvXKztPp5PA", "type": "fieldset" }
        }
      }
    },
    {
      "id": "bwk17lanRYCOvXKztPp5PA",
      "type": "fieldset",
      "attributes": {
        "title": "Group 1",
        // ... the rest of fieldset attributes
      },
      "relationships": {
        "item_type": {
          "data": { "id": "UVP2y5QPToWPXqJbMszyFg", "type": "item_type" }
        }
      }
    }
  ]
}
```

### Records

For each schema model, multiple files following this template are present in the bucket:

```plaintext
content/project_<ID>/snapshot_<TIMESTAMP>/records/<schema_model_api_key>/current/batch_<batch_increment_number>.json
content/project_<ID>/snapshot_<TIMESTAMP>/records/<schema_model_api_key>/published/batch_<batch_increment_number>.json
```

For instance:

```plaintext
content/project_999/snapshot_1721033044/records/article/current/batch_000.json
content/project_999/snapshot_1721033044/records/article/current/batch_001.json
content/project_999/snapshot_1721033044/records/article/published/batch_000.json
content/project_999/snapshot_1721033044/records/article/published/batch_001.json
```

The `current` prefix contains the records' current versions (the latest version available, as seen in the admin interface). The `published` prefix contains records' published versions.

The same record ID can be present in both trees (`current` and `published`) if it has both a current and a published version. The same version can be present in both trees if it's at the same time the current and published version of the record.

Versions include their block records, similar to using the `nested=true` query parameter in our Content Management API.

Each `batch_XXX.json` contains several versions under the `data` key and their order is not predictable.

```json5
{
  "data": [
    {
      "id": "ZrKQnn5AQBiZ4CTX8eyu8Q",
      "type": "item",
      "attributes": {
        "title": "A trip to Florence!",
        "content": {
          "en": [
            {
              "type": "item",
              "attributes": {
                "text": "Beautiful!"
                // ... the rest of block record attributes
              },
              "relationships": {
                "item_type": {
                  "data": {
                    "id": "JfkKRx-FRJONbco_hHOS5Q",
                    "type": "item_type"
                  }
                }
              },
              "id": "afzDcUT0RHOduJbr8L_ZmA"
            }
          ]
        }
        // ... the rest of record attributes
      },
      "relationships": {
        "item_type": {
          "data": { "id": "UVP2y5QPToWPXqJbMszyFg", "type": "item_type" }
        },
        "creator": { "data": { "id": "24527", "type": "account" } }
      },
      "meta": {
        // ...
      }
    },
    {
      // .. other current versions
    }
  ]
}
```

---

# Import and Export — Import space from Contentful

Source [docs]: https://www.datocms.com/docs/import-and-export/import-space-from-contentful.md

If you want to try DatoCMS, but you created your existing project with Contentful, you can use our command-line tool to import all content from a Contentful space to a DatoCMS project.

(Video content)

### Setup

First install the `datocms` npm package:

Terminal window

```bash
npm install -g datocms
```

The package exposes the `datocms` CLI command, that you can use to install the Contentful importer plugin:

Terminal window

```bash
npx datocms plugins:install @datocms/cli-plugin-contentful
```

### What you will need

To copy your Contentful space to DatoCMS, you will need the following information:

**Your Contentful Space ID:** you can find it under *Settings \> General settings*:

(Image content)

A **Contentful content management token**: You can create one under *Settings ⛭ (gear icon) \> CMA tokens* and then clicking the *Create personal access token* button:

(Image content)

Creating a Contentful personal access token

Once it's created, be sure to copy the token — it's the only time you'll see it.

Then you'll need to Authorize it:

(Image content)

Authorizing the Contentful CMA token

Your **DatoCMS full-access API token**: first create a new project, then go to *Project settings \> API tokens*, click on **"Add a new access token"**, and choose or create a **role** with read-write permissions. You can select the default *Admin* role, or create a more granular one, depending on your needs.

(Image content)

### Run the import

To import all the entries and assets of your Contentful space into DatoCMS, run the following in the console, making sure to replace the placeholder values with the tokens and IDs of your project:

Terminal window

```bash
rm -rf ./api-calls && datocms contentful:import \
  --api-token=<apiToken> \
  --contentful-token=<apiToken> \
  --contentful-space-id=<spaceId> \
  --log-level=BODY_AND_HEADERS \
  --log-mode=directory
```

By specifying the `log-level` and `log-mode` options, a complete list of API calls made both to Contentful and DatoCMS will be generated in the `./api-calls` folder, one per file, in chronological order. This information can be of great help if something should go wrong during the import.

If desired, you can also specify the `--ignore-errors` option, which will attempt to continue with the import process even if it encounters errors along the way.

The required parameters are these:

Terminal window

```bash
--api-token=<value>             Your DatoCMS project read-write API token
--contentful-space-id=<value>   Your Contentful space ID
--contentful-token=<value>      Your Contentful read-write API token
```

To view the full list of options, you can always run the command:

Terminal window

```bash
npx datocms contentful:import --help
```

### Known limitations

Although highly compatible, there are some minor differences between the types of fields that Contentful offers compared to DatoCMS, so the tool will follow these migration rules:

-   DatoCMS doesn't provide an array of strings field, so data of this kind will be converted in a single string field with comma separated values;
-   Contentful API doesn't expose presentation settings for fields, so all text fields will be set as Markdown editors (you will be able to change the presentation mode later from the DatoCMS interface);
    
-   DatoCMS doesn't allow a multi-paragraph text field to be the Model title, so if that's the case, no title field will be set;
-   While Contentful's reference field allows not specifying the list of content types that can be referenced, DatoCMS instead requires an explicit list. Therefore, in these cases, the task will set the entire catalog of models as the explicit list.

---

# Import and Export — Import from WordPress

Source [docs]: https://www.datocms.com/docs/import-and-export/import-from-wordpress.md

In this guide we'll go through the import of content present in a WordPress site to a DatoCMS project.

### Installation

Install the DatoCMS CLI:

Terminal window

```bash
npm install -g datocms@latest
```

And subsequently install the WordPress importer plugin:

Terminal window

```bash
$ datocms plugins:install @datocms/cli-plugin-wordpress
```

### What you will need

To copy your WordPress content to DatoCMS, you will need the following information:

1.  Your WordPress user name and password with **admin privileges**
    
2.  Your WordPress site URL
    
3.  Your DatoCMS full-access API token: first create a new project, then go to *Project settings \> API tokens*, click on **"Add a new API token"**, and choose or create a **role** with read-write permissions. You can select the default *Admin* role, or create a more granular one, depending on your needs.
    

(Image content)

### Run the import

To import the posts and pages of your WordPress project into DatoCMS, run the following in the console:

Terminal window

```bash
$ datocms wordpress:import \
          --ignore-errors \
          --wp-url <YOUR_WP_PROJECT_URL> \
          --wp-username <YOUR_WP_USERNAME> \
          --wp-password <YOUR_WP_PASSWORD> \
          --api-token <YOUR_DATOCMS_API_TOKEN>
```

That's it! The importer will create the standard Wordpress models: articles, pages, authors, categories and tags. All the Wordpress media files will be uploaded to your DatoCMS project in the media gallery as well. Hurray!

(Video content)

### Known limitations

-   Our importer only copies pages and posts: custom post types won't be imported;
-   There are many different plugins to manage localizations in WordPress-land. For now, if you have a multi-lingual website, we’ll currently only import the content created for the main language.
    
-   Same thing goes for SEO, sliders and other web elements managed by plugins. They won’t be imported.

---

# Import and Export — Importing data from other sources

Source [docs]: https://www.datocms.com/docs/import-and-export/importing-data.md

As a developer working with DatoCMS, you often find yourself in need of importing data from an external source. For example when you are doing a one-time import from another CMS to DatoCMS, or when you just want to clean up messy data from an external API or RESTful web service, or when you want the ability to perform powerful queries on it.

In this guide we will cover how to do a one-time import from an external data source using Node.

**Concepts you should be familiar with:** knowledge of Node.js and `async`/`await`.

**What are some common external sources?** An external data source can come in a wide range of different formats made available on different transport layers. Here's a few examples:

-   The REST API of your old CMS
-   A text file with comma separated values (CSV)
    
-   A SQL database
-   A JSON file or newline delimited JSON (NDJSON) file
    

### The anatomy of an external data import

No matter what kind of source you are reading from, an external import can be split into three discrete steps:

-   Read data from the external source
-   Transform the data to DatoCMS records(s) matching your data model
    
-   Save the records to your DatoCMS project
    

We will cover each of these in order

### Step 1. Read data from the external source

Let's start with a simple example where the external data source is an API endpoint containing an array of breeds of dogs that we want to import into a DatoCMS project.

```json5
[
  {
    "id": 1,
    "breed": "Alapaha Blue Blood Bulldog",
    "bred_for": "Guarding",
    "category": "Mixed",
    "description": "The Alapaha Blue Blood Bulldog is a well-developed, exaggerated bulldog with a broad head and...",
    "life_span": "12 - 13 years",
    "image_url": "https://cdn2.thedogapi.com/images/kuvpGHCzm.jpg"
  },
  {
    "id": 2,
    "breed": "Alaskan Husky",
    "bred_for": "Sled pulling",
    "category": "Mixed",
    "life_span": "10 - 13 years",
    "image_url": "https://cdn2.thedogapi.com/images/uEPB98jBS.jpg"
  },
  {
    "id": 3,
    "breed": "Alaskan Malamute",
    "bred_for": "Hauling heavy freight, Sled pulling",
    "category": "Working",
    "life_span": "12 - 15 years",
    "image_url": "https://cdn2.thedogapi.com/images/aREFAmi5H.jpg"
  },
  ...
]
```

The quickest way to read from this API in Node.js is to install the `node-fetch` package which gives you a `window.fetch`\-like API that enables you to fetch the data:

```javascript
const fetch = require('node-fetch');

async function importDogBreeds() {
  const response = await fetch('https://something.now.sh/dog-breeds');
  const dogBreeds = await response.json();

  // we now have an array of dogBreeds from the external API
}

importDogBreeds();
```

### Step 2: Transform to DatoCMS record(s) matching your data model

Now, let's say the following is the DatoCMS schema we want our imported data to adhere to:

##### Model "Category"

-   ID: `552`
-   API key: `category`
    
-   Fields:
    
    -   Name (API key: `name`): string
        

##### Model "Dog breed"

-   ID: `730`
-   API key: `dog_breed`
    
-   Model fields:
    
    -   Name (API key: `name`): string
        
    -   Category (API key: `category`): link to model `category`
        
    -   Breed for (API key: `breed_for`): string
        
    -   Description (API key: `description`): text
        
    -   Image (API key: `image`): file
        

If you look carefully, you'll see that the source data doesn't map 1:1 to the schema model. There's a few differences to note here:

-   The `breed` field is called `name` in our DatoCMS model
-   Instead of importing `category` directly as text inside the breed, we want to create a separate record for them, and have the `category` field be a reference to it instead;
    
-   The `life_span` field from the external API isn't relevant to us, and we don't want to import it at all;
    

This can roughly be codified to the following transform function:

```javascript
function transformDogBreed(externalData) {
  return {
    item_type: { type: 'item_type', id: '730' }, // <- that's the ID of our dog_breed model
    name: externalData.breed,
    category: ???,
    breed_for: externalData.breed_for,
    description: externalData.description,
    image: ???,
  };
}
```

As you might have guessed, `item_type` means "model" in DatoCMS APIs, and you have to fill it in with the ID of your model (in this case, `"730"`).

The `category` field requires a category record ID, but right now we do not have it. This suggests us that first we have to import the breed categories, and then we can proceed with importing the dog breeds.

To do that, we get all the different dog breed categories, and then we remove any duplicate:

```javascript
const uniq = require('lodash.uniq');
const fetch = require('node-fetch');

async function importDogBreeds() {
  const response = await fetch('https://something.now.sh/dog-breeds');
  const dogBreeds = await response.json();

  const categories = dogBreeds.map(dogBreed => dogBreed.category)
  const uniqueCategories = uniq(categories);
}
```

### Step 3: Importing to DatoCMS

In the previous steps all we did was fetch and prepare the data to be imported into your DatoCMS project. Now it's time to actually make it become DatoCMS records.

First we need to configure our [DatoCMS client](/docs/content-management-api/using-the-nodejs-clients.md) with our project's API token. We will need to add `@datocms/cma-client-node` as a dependency to our project and create a client instance:

```javascript
import { Client } from '@datocms/cma-client-node';

const client = new Client({ apiToken: '<YOUR-TOKEN-WITH-WRITE-ACCESS>' })
```

In order to give this client write access, we need to generate an access token. You can generate an access token under the "API token" section of your project's settings.

(Image content)

Now that we have our client configured, the next step is to create our records, using the `client.items.create` method:

```javascript
const categoryNameToRecord = {};

for (let categoryName of uniqueCategories) {
  categoryNameToRecord[name] = await client.items.create({
    item_type: { type: 'item_type', id: '552' }, // <- that's the ID of our category model
    name
  });
}
```

As you can see, we save the created records in a `categoryNameToRecord` object so that it will be easier to access them during the creation of dog breeds, which is obviously the next thing we need to to do in our script:

```javascript
for (let dogBreed of dogBreeds) {
  categoryNameToRecord[name] = await client.items.create({
    itemType: { type: 'item_type', id: '730' }, // <- that's the ID of our dog_breed model
    name: externalData.breed,
    category: categoryNameToRecord[dogBreed.category].id, // <- we pick the ID of our category record
    breed_for: externalData.breed_for,
    description: externalData.description,
    image: ???,
  });
}
```

The last step is uploading the images. To do that, we can simply use the `client.uploads.createFromUrl` method, passing down additional data such as the default alternate text we want for each image. You can learn more in our [CMA docs](/docs/content-management-api/resources/item/create.md#assets):

```javascript
for (let dogBreed of dogBreeds) {
  const upload = await client.uploads.createFromUrl({
    url: dogBreed.image_url,
    default_field_metadata: {
      en: {
        alt: `${dogBreed} dog`,
      },
    },
  });

  categoryNameToRecord[name] = await client.items.create({
    // ...
    image: { upload_id: upload.id },
  });
}
```

And voilà! You've just successfully imported your external data to DatoCMS!

---

# Custom asset domains — Custom Domain Name for Assets (Enterprise only)

Source [docs]: https://www.datocms.com/docs/custom-asset-domains.md

## What are custom asset domains?

Since [DatoCMS is a headless CMS](https://www.datocms.com/academy/headless-cms/how-a-headless-cms-works.md), your website's domain and URL structure are typically up to you to define, like `example.com/posts`. However, your project's "assets" (uploaded files like images and PDFs) are an exception to this rule. Normally, they are hosted on a domain name shared by all our customers.

DatoCMS projects on the Professional and Free plans use the shared domain name `www.datocms-assets.com` to serve assets to website visitors. The assets are hosted on our cloud storage and cached by our image CDN partner, [Imgix](https://www.imgix.com/), with an example public URL like `https://www.datocms-assets.com/205/1603211008-whitefulllogo.png`.

[**Enterprise customers**](https://www.datocms.com/enterprise-headless-cms.md) **can (optionally) use their own cloud storage and domain name instead,** resulting in prettier URLs like `https://example.com/images/whitefulllogo.png`.

#### Requirements

1.  Custom domain names are **only available for DatoCMS Enterprise customers**, not Professional or Free users. See our [plan comparison](https://www.datocms.com/pricing.md).
    
2.  Assets must be hosted on a **customer-provided** [**AWS S3**](https://www.datocms.com/marketplace/enterprise/aws-s3.md)**,** [**Azure Blob**](https://www.datocms.com/marketplace/enterprise/azure-blob-storage.md)**,** [**Google Cloud Storage**](https://www.datocms.com/marketplace/enterprise/google-cloud-storage.md)**,** or[**Cloudflare R2**](https://www.datocms.com/marketplace/enterprise/cloudflare-r2.md)bucket under a **separate subscription** directly with that cloud vendor, existing or new.
    
3.  An [**Imgix Premium Plan with custom SSL**](https://docs.imgix.com/en-US/getting-started/setup/creating-sources/advanced-settings#custom-domains) is also required. This will also be a **separate account** you subscribe to directly through Imgix.
    

#### Pricing

This service is included as part of DatoCMS Enterprise plans. However, you may need to separately subscribe to and pay for:

-   Cloud storage costs with an external provider
-   An Imgix premium plan with custom SSL
    
-   Bandwidth charges to/from the cloud storage (their ingress/egress fees)
-   Bandwidth charges from Imgix to your visitors (depending on your plan with them)
    

#### Additional Info

-   At this time, we cannot provide custom domain names for assets on our own cloud storage. You must use one of the external storage options and an Imgix premium plan. Please see [Requirements](/docs/custom-asset-domains.md#requirements) above for details.
-   Your editors' user experience inside the media area should not change. This is a backend configuration change only.
    
-   If you already have existing assets (uploaded files) in your media area, we can help you migrate them over to a new storage service.
-   Nearly all [Imgix URL parameters](https://docs.imgix.com/en-US/apis/rendering/overview) will continue to work as before, with the notable exception of the [DatoCMS-specific `skip-default-optimizations` parameter](/docs/asset-api/asset-cdn-settings.md#automatic-image-optimization). Instead, you can [specify your own default parameters](https://docs.imgix.com/en-US/getting-started/setup/creating-sources/advanced-settings#default-parameters) on your Imgix source.
    

### Next steps: Enabling Custom Domains

1.  If you're not already on an Enterprise plan, please see our [pricing page](https://www.datocms.com/pricing.md) for details and then [contact our Sales team](https://www.datocms.com/contact.md) to sign up.
    
2.  Then, please choose a cloud storage provider and follow instructions below to set it up as your new Imgix asset source:
    
    -   [AWS S3 bucket](https://www.datocms.com/marketplace/enterprise/aws-s3.md)
        
    -   [Azure Blob](https://www.datocms.com/marketplace/enterprise/azure-blob-storage.md)
        
    -   [Google Cloud Storage](https://www.datocms.com/marketplace/enterprise/google-cloud-storage.md)
        
    -   [Cloudflare R2](https://www.datocms.com/marketplace/enterprise/cloudflare-r2.md)
        
3.  Once step 2 is complete, please email [support@datocms.com](mailto:support@datocms.com) so we can help you finalize the setup.
    

### **Questions?**

If anything is unclear, please reach out to us at [support@datocms.com](mailto:support@datocms.com) for technical assistance. You can also [contact our Sales team](https://www.datocms.com/contact.md) for inquiries about Enterprise pricing.

---

# Next.js — Next.js + DatoCMS Overview

Source [docs]: https://www.datocms.com/docs/next-js.md

Next.js is an exceptional tool for building modern, universal frontend applications with the power of React. It lets you get started without having to write much boilerplate code and with a set of sane defaults upon which you can build.

[Vercel](https://vercel.com/solutions/nextjs) is the easiest way to deploy a production-ready, highly available Next.js website, with static assets being served through the CDN automatically and built-in support for Next.js’ automatic static optimization and API routes.

DatoCMS is the perfect companion to Next.js since it offers content, images and videos on a globally-distributed CDN, much like Vercel does for the static assets of your website. With this combo, you can have an **infinitely scalable website, ready to handle prime-time TV traffic spikes, at a fraction of the regular cost.**

> [!NOTE] Still using the old Pages Router?
> If you're still using the [Pages Router](https://nextjs.org/docs/pages) — that is, the features available under `/pages` — please follow [this documentation](/docs/legacy-next-js-documentation.md) instead.

#### Project starters

Our [marketplace](https://www.datocms.com/marketplace/starters.md) features different demo projects on Next, so you can learn and get started easily:

[

(Image content)

Next.js Starter Kit

Try this demo »

](https://www.datocms.com/marketplace/starters/next-js-starter-kit.md)[

(Image content)

Marketing Website

Try this demo »

](https://www.datocms.com/marketplace/starters/marketing-website.md)[

(Image content)

Ecommerce Website

Try this demo »

](https://www.datocms.com/marketplace/starters/ecommerce-website.md)

#### Tutorials

Our Community has also created many great video tutorials you can follow:

[

(Image content)

Next.js + DatoCMS tutorial for beginners

Play video »

](https://www.youtube.com/watch?v=_VIF1if-dNA)

[

(Image content)

Build a dynamic landing page with Next.js and Tailwind CSS

Play video »

](https://www.youtube.com/watch?v=it5nNneptgM)

[

(Image content)

How to use Next.js On-Demand ISR with DatoCMS webhooks

Play video »

](https://www.youtube.com/watch?v=Wh3P-sS1w0I)

## Quick start

First, create a new Next.js application using create-next-app, which sets up everything automatically for you.

To create a project, run the following command and follow the wizard:

Terminal window

```bash
npx create-next-app@latest
```

Then enter the project directory and start the development server:

Terminal window

```bash
cd my-app
npm run dev
```

### Fetching content from DatoCMS

When it comes to fetching data, Next recommends the following:

-   [perform the fetch on the Server](https://nextjs.org/docs/app/building-your-application/data-fetching#fetching-data-on-the-server), to reduce the back-and-forth communication between client and server;
-   [use Next.js `fetch` API](https://nextjs.org/docs/app/building-your-application/data-fetching#the-fetch-api), and call it whenever you need it, be it a layout, a page or a specific component.
    

Let's start by installing `@datocms/cda-client`, a lightweight, TypeScript-ready package that offers various helpers around the native Fetch API to perform GraphQL requests towards [DatoCMS Content Delivery API](/docs/content-delivery-api/api-endpoints.md):

Terminal window

```bash
npm install --save @datocms/cda-client
```

We can now create a function we can use in all of our components that need to fetch content from DatoCMS: Create a new directory called `lib`, and inside of it, add a file called `datocms.js`:

lib/datocms.js

```jsx
import { executeQuery } from '@datocms/cda-client';

export const performRequest = (query, options) => {
  return executeQuery(query, {
    ...options,
    token: process.env.NEXT_DATOCMS_API_TOKEN,
    environment: process.env.NEXT_DATOCMS_ENVIRONMENT,
  });
}
```

> [!WARNING] Enhanced Data Fetching
> While the above function works for simple cases, we strongly suggest to take a look at the next section, where we cover more details about data fetching, and [introduce a more flexible and optimized `performRequest()`.](/docs/next-js/optimizing-calls-with-react-cache-function.md#our-improved-performrequest)

You can see that to build the right authentication header, we're using an environment variable prefixed by `NEXT_` . To create the API token for a DatoCMS project, go in the "Settings \> API Tokens" section, making sure you only give it permission to access the **Content Delivery API** and the **Content Delivery API with draft content:**

(Video content)

Next, go to `app/page.js` — that is, the component that renders the homepage of our project — define the GraphQL query to be executed, and in the component use the `performRequest()` function to perform the request:

```jsx
import { performRequest } from 'lib/datocms';

const PAGE_CONTENT_QUERY = `
  query Home {
    homepage {
      title
      description {
        value
      }
    }
  }`;

export default async function Home() {
  const { homepage } = await performRequest(PAGE_CONTENT_QUERY);

  // [...]
}
```

The `PAGE_CONTENT_QUERY` is the GraphQL query, and of course, it depends on the models available in your specific DatoCMS project.

You can learn everything you need regarding how to build GraphQL queries on our [Content Delivery API documentation](/docs/content-delivery-api.md).

---

# Next.js — Optimizing calls to DatoCMS

Source [docs]: https://www.datocms.com/docs/next-js/optimizing-calls-with-react-cache-function.md

## Next.js 15 and Later

Starting with Next.js 15, `fetch` is no longer auto-cached. It is now an opt-in mechanism. Please see the [Next 15 caching docs](https://nextjs.org/docs/15/app/guides/caching) for details.

## Next.js 14

Although the Next.js `fetch` API has (almost) the same interface as the regular `fetch` available on the browser, it is important to **highlight some key differences**, which might cause some surprise.

### Next.js 14 automatically caches fetches

By default, Next.js [automatically caches your fetches](https://nextjs.org/docs/14/app/building-your-application/data-fetching/fetching-caching-and-revalidating#caching-data):

-   For `fetch` calls happening in Server Components, this means that the data will be **fetched at build time, cached, and reused indefinitely on each request until your next deploy.**
-   For `fetch` calls happening in Client Components, the cache **lasts the duration of a session** (which could include multiple client-side re-renders) before a full page reload.
    

Caching requests is generally a good idea, as it minimizes the number of requests made to DatoCMS. However, if you want to always fetch the latest data, you can mark requests as *dynamic* and fetch data on each request without caching.

### GraphQL calls need to be manually cached

The automatic caching only works for `GET` requests. Since GraphQL requests use a `POST` HTTP action, we need to manually handle CDA caching ourselves.

For this purpose, we can use a useful helper that React offers called `cache`, which memoizes the result of the passed function: [Next.js: React Cache Function](https://nextjs.org/docs/14/app/building-your-application/caching#react-cache-function).

### Our improved `performRequest`

Based on what we have just learned, we can refine our `performRequest` function, and make it more flexible and optimized:

```jsx
import { executeQuery } from '@datocms/cda-client';
import { cache } from 'react';

const dedupedPerformRequest = cache(async (serializedArgs) => {
  return executeQuery(...JSON.parse(serializedArgs));
})

export function performRequest(query, options) {
  return dedupedPerformRequest(JSON.stringify([
    query,
    {
      ...options,
      token: process.env.NEXT_DATOCMS_API_TOKEN,
      environment: process.env.NEXT_DATOCMS_ENVIRONMENT,
    },
  ]);
}
```

This new version dedupes your GraphQL requests, supports all [CDA header modes](/docs/content-delivery-api/api-endpoints.md), and lets you control if — and for how long — you want to cache the result of the query with the [`revalidate` option](https://nextjs.org/docs/app/api-reference/functions/fetch#optionsnextrevalidate):

```jsx
// cache the query result indefinitely (until next deploy)
await performRequest(query);

// cache the query result for a maximum of 60 seconds
await performRequest(query, requestInitOptions: { next: { revalidate: 60 } });
```

---

# Next.js — Managing images

Source [docs]: https://www.datocms.com/docs/next-js/managing-images.md

One of the major advantages of using DatoCMS instead of any other content management systems is its [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images), which returns **pre-computed image attributes that will help you setting up responsive images in your frontend without any additional manipulation**.

To make it even easier to offer responsive, progressive images on your projects, we offer a package called [`react-datocms`](https://github.com/datocms/react-datocms) that exposes two components pairing perfectly with the `responsiveImage` query: `<Image/>` and `<SRCImage/>.`

Our solution offers the same advantages as using the Next.js [Image component](https://nextjs.org/docs/basic-features/image-optimization), with the added benefit of having beautiful low-quality image placeholders (LQIP) in base64 format embedded directly within the page, without any additional request to be made by the browser or server:

(Video content)

To take advantage of it, install the [`react-datocms`](https://github.com/datocms/react-datocms) package:

Terminal window

```bash
npm install react-datocms
```

Then, inside your page, feed content coming from a [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images) directly into the `<Image />` component:

```jsx
import { Image as DatoImage, SRCImage as DatoSRCImage } from "react-datocms";
import { performRequest } from '../lib/datocms';

const PAGE_CONTENT_QUERY = `query HomePage($limit: IntType) {
  allBlogPosts(first: $limit) {
    id
    title
    coverImage {
      responsiveImage(imgixParams: { fit: crop, w: 300, h: 300, auto: format }) {
        sizes
        src
        width
        height
        alt
        title
        base64
      }
    }
  }
}`;

export default async function Home() {
  const pageContent = await performRequest(PAGE_CONTENT_QUERY, {
    variables: { limit: 10 },
  });

  return (
    <div>
      {data.allBlogPosts.map(blogPost => (
        <article key={blogPost.id}>
          {/* client component with custom lazy-loading via IntersectionObserver */}
          <DatoImage data={blogPost.coverImage.responsiveImage} />
          {/* server component, uses native loading="lazy" */}
          <DatoSRCImage data={blogPost.coverImage.responsiveImage} />
          <h2>{blogPost.title}</h2>
        </article>
      ))}
    </div>
  );
}
```

### `<SRCImage />` vs `<Image />`

Even though their purpose is the same, there are some significant differences between these two components. Depending on your specific needs, you can choose to use one or the other:

-   `<SRCImage />` is a [React Server Component](https://nextjs.org/docs/app/building-your-application/rendering/server-components), so it can be rendered and optionally cached on the server. It doesn't create any JS footprint. It generates a single `<picture />` element and implements lazy-loading using the native [`loading="lazy"` attribute](https://web.dev/articles/browser-level-image-lazy-loading). The placeholder is set as the background to the image itself. Be careful: the placeholder is not removed when the image loads, so it's not recommended to use this component if you anticipate that the image may have an alpha channel with transparencies.
-   `<Image />` is a [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components). Since it runs on the browser, it has the ability to set a cross-fade effect between the placeholder and the original image, but at the cost of generating more complex HTML output composed of multiple elements around the main `<picture />` element. It also implements lazy-loading through `IntersectionObserver`, which allows customization of the thresholds at which lazy loading occurs.
    

We recommend that you delve deeper into the topic in the [documentation of the components themselves](https://github.com/datocms/react-datocms/blob/master/docs/image.md).

---

# Next.js — Displaying videos

Source [docs]: https://www.datocms.com/docs/next-js/displaying-videos.md

> [!PROTIP] Pro tip: Start with our how-to guides first
> If you're new to hosting videos on DatoCMS, we recommend first starting with our tutorials:
> 
> -   How to upload videos: [Videos and Video Optimizations](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)
>     
> -   Why you should use HLS Streaming via Mux: [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md)
>     
> 
> Then, this page provides framework-specific playback advice using our helper components. Read on when you're ready!

One of the advantages of using DatoCMS instead of other content management systems is its `video` query, which will return **pre-computed video attributes that will help you display videos in your frontend without any additional manipulation**.

To make it easy to offer optimized, progressive videos on your projects, we offer a package called [`react-datocms`](https://github.com/datocms/react-datocms) that exposes a `<VideoPlayer />` component and pairs perfectly with the video query.

To take advantage of it, install the [`react-datocms`](https://github.com/datocms/react-datocms) package:

```plaintext
npm install react-datocms
```

Then, inside your page, feed content coming from a `video` query directly into the `<VideoPlayer />` component:

```javascript
import { VideoPlayer } from "react-datocms";
import { performRequest } from '../lib/datocms';

const PAGE_CONTENT_QUERY = `query HomePage($limit: IntType) {
  allBlogPosts(first: $limit) {
    id
    title
    coverVideo {
      video {
        muxPlaybackId
        title
        width
        height
        blurUpThumb
      }
    }
  }
}`;

export default async function Home() {
  const pageContent = await performRequest(PAGE_CONTENT_QUERY, {
    variables: { limit: 10 },
  });

  return (
    <div>
      {data.allBlogPosts.map(blogPost => (
        <article key={blogPost.id}>
          <VideoPlayer data={blogPost.coverVideo.video} />
          <h2>{blogPost.title}</h2>
        </article>
      ))}
    </div>
  );
}
```

---

# Next.js — Structured Text fields

Source [docs]: https://www.datocms.com/docs/next-js/rendering-structured-text-fields.md

Rich text in DatoCMS is stored in [**Structured Text**](/docs/content-modelling/structured-text.md) **fields**, which lets us use it in many different contexts, from HTML in the browser to speech fulfillments in voice interfaces, if that's what you want.

There's a lot to be said about Structured Text and the extensibility of it, but for now let's just say that it returns content in a particular [JSON format called `dast`](/docs/structured-text/dast.md) which will resemble this example:

```json
{
  "schema": "dast",
  "document": {
    "type": "root",
    "children": [
      {
        "type": "heading",
        "level": 1,
        "children": [
          {
            "type": "span",
            "marks": [],
            "value": "Hello world!"
          }
        ]
      }
    ]
  }
}
```

To make it easy to convert this format in HTML inside your Next.js projects, we released a package called [`react-datocms`](https://github.com/datocms/react-datocms) that exposes a `<StructuredText />` component that does all the heavy lifting for you.

To take advantage of it, install the [`react-datocms`](https://github.com/datocms/react-datocms) package if you haven't already:

Terminal window

```bash
npm install react-datocms
```

Then, inside your page, make a [GraphQL query to fetch a Structured Text field](/docs/content-delivery-api/structured-text-fields.md), and feed the result to the `data` prop of a `<StructuredText />` component:

```jsx
import { StructuredText } from "react-datocms";
import { performRequest } from 'lib/datocms';

const PAGE_CONTENT_QUERY = `
  query HomePage($limit: IntType) {
    allBlogPosts(first: $limit) {
      id
      title
      content {
        value
      }
    }
  }`;

export default async function Home() {
  const pageContent = await performRequest(PAGE_CONTENT_QUERY, {
    variables: { limit: 10 }
  });

  return (
    <div>
      {data.allBlogPosts.map(blogPost => (
        <article key={blogPost.id}>
          <h2>{blogPost.title}</h2>
          <StructuredText data={blogPost.content} />
        </article>
      ))}
    </div>
  );
}
```

## Rendering special nodes

Other than "regular" formatting nodes (paragraphs, lists, etc.), Structured Text documents can contain four special types of node:

-   [`itemLink` nodes](/docs/structured-text/dast.md#itemLink) are just like regular HTML hyperlinks, but point to other records instead of URLs;
-   [`inlineItem` nodes](/docs/structured-text/dast.md#inlineItem) lets you directly embed a reference to a record in-between regular text;
    
-   [`block` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular paragraphs;
-   [`inlineBlock` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular text;
    

If a Structured Text document contains one of these nodes, then we need to change the GraphQL query, so that we also fetch all the records and blocks it references. As an example, if the field can link to other Blog posts, and can embed blocks of type "Image block" and "Mention block", then the query should change like this:

```jsx
const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  allBlogPosts(first: $limit) {
    id
    title
    content {
      value
      blocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on ImageBlockRecord {
          image { url alt }
        }
      }
      inlineBlocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on MentionBlockRecord {
          username
        }
      }
      links {
        ... on RecordInterface {
          id
          __typename
        }
        ... on BlogPostRecord {
          slug
          title
        }
      }
    }
  }
}`;
```

We also need to tell `<StructuredText />` how you want such nodes to be rendered:

```jsx
return (
  <StructuredText
    data={blogPost.content}
    renderInlineRecord={({ record }) => {
      switch (record.__typename) {
        case "BlogPostRecord":
          return <a href={`/blog/${record.slug}`}>{record.title}</a>;
        default:
          return null;
      }
    }}
    renderLinkToRecord={({ record, children }) => {
      switch (record.__typename) {
        case "BlogPostRecord":
          return <a href={`/blog/${record.slug}`}>{children}</a>;
        default:
          return null;
      }
    }}
    renderBlock={({ record }) => {
      switch (record.__typename) {
        case "ImageBlockRecord":
          return <img src={record.image.url} alt={record.image.alt} />;
        default:
          return null;
      }
    }}
    renderInlineBlock={({ record }) => {
      switch (record.__typename) {
        case "MentionBlockRecord":
          return <code>@{record.username}</code>;
        default:
          return null;
      }
    }}
  />
);
```

To see structured text in action with Next.js, check out this tutorial:

[

(Image content)

How to use Structured Text fields with Next.js

Play video »

](https://www.youtube.com/watch?v=aKZJnqLialw)

---

# Next.js — Adding SEO to pages

Source [docs]: https://www.datocms.com/docs/next-js/seo-management.md

Similarly to what we offer with [responsive images](/docs/next-js/managing-images.md), our GraphQL API also offers a way to fetch [**pre-computed SEO meta tags**](/docs/content-delivery-api/seo-and-favicon.md) **based on the content you insert inside DatoCMS**.

You can easily use this information inside your Next app with the help of our [`react-datocms`](https://github.com/datocms/react-datocms) package.

Here's a sample of the meta tags you can automatically generate:

```html
<title>DatoCMS Blog - DatoCMS</title>
<meta property="og:title" content="DatoCMS Blog" />
<meta name="twitter:title" content="DatoCMS Blog" />
<meta name="description" content="Lorem ipsum..." />
<meta property="og:description" content="Lorem ipsum..." />
<meta name="twitter:description" content="Lorem ipsum..." />
<meta property="og:image" content="https://www.datocms-assets.com/..." />
<meta property="og:image:width" content="2482" />
<meta property="og:image:height" content="1572" />
<meta name="twitter:image" content="https://www.datocms-assets.com/..." />
<meta property="og:locale" content="en" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DatoCMS" />
<meta property="article:modified_time" content="2020-03-06T15:07:14Z" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@datocms" />
<link sizes="16x16" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="32x32" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="96x96" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="192x192" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
```

To use this feature, install the [`react-datocms`](https://github.com/datocms/react-datocms) package:

Terminal window

```bash
npm install react-datocms
```

Then, inside your page, feed content coming from a `faviconMetaTags` or `_seoMetaTags` query directly into the `toNextMetadata` function:

```jsx
import { toNextMetadata } from "react-datocms";
import { performRequest } from 'lib/datocms';

import Head from "next/head";

const PAGE_CONTENT_QUERY = `{
  site: _site {
    favicon: faviconMetaTags {
      attributes
      content
      tag
    }
  }
  blog {
    seo: _seoMetaTags {
      attributes
      content
      tag
    }
    title
  }
}`;

function fetchContent() {
  return performRequest(PAGE_CONTENT_QUERY, {
    variables: { limit: 10 }
  });
}

export async function generateMetadata() {
  const { site, blog } = await fetchContent();

  return toNextMetadata([ ...site.favicon, ..blog.seo ])
}

export default function Home() {
  const { blog } = await fetchContent();

  // [...]
}
```

Want to know more about SEO customization in DatoCMS? Check out this video tutorial:

[

(Image content)

Working with and customizing SEO Fields

Play video »

](https://youtu.be/WjF10isSjS0)

---

# Next.js — Setting up Next.js Draft Mode

Source [docs]: https://www.datocms.com/docs/next-js/setting-up-next-js-draft-mode.md

Static rendering is useful when your pages fetch data from a headless CMS. However, it’s not ideal when you’re [writing a draft on DatoCMS](/docs/general-concepts/draft-published.md) and want to view the draft immediately on your page.

Next.js has a feature called [Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode) , which solves this problem. Here’s a guide on how to use it.

#### Step1: Create a draft mode API route

First, create a preview API route. It can have any name — e.g. `app/api/draft/route.ts`. In this API route, you must call `draftMode().enable()` to enable draft mode.

app/api/draft/route.js

```javascript
import { draftMode } from 'next/headers';

export async function GET(request) {
  draftMode().enable();
  redirect('/');
}
```

You can manually test this route by accessing it via a browser at `http://localhost:3000/api/draft`. You’ll notice that you'll be redirected to the homepage, and the `__prerender_bypass` cookie will be set.

#### Step 2: Conditionally include draft records

Once draft mode is setup, your pages can check whether it is active or not with the `draftMode().isEnabled` property, and use this information to tweak the call to `performRequest` so that the `includeDrafts` flag is turned on.

This will instruct DatoCMS to [return records at their latest version available](/docs/content-delivery-api/api-endpoints.md#include-drafts) instead of the currently published one:

```jsx
import { draftMode } from 'next/headers';

export default async function Page() {
  const { isEnabled } = draftMode();

  const { data: { homepage } } = await performRequest(PAGE_CONTENT_QUERY, {
    includeDrafts: isEnabled,
  });

  // [...]
}
```

You can read more details regarding draft mode on [Next.js docs page](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode).

---

# Next.js — Real-time updates

Source [docs]: https://www.datocms.com/docs/next-js/real-time-updates.md

Live updates can be extremely useful both for content editors and regular visitors of your app/website:

-   Content-editors in Draft Mode can **see their work-in-progress directly in the production website**, without having to refresh the page;
-   Visitors can **immediately see new content as it gets published**, allowing all kinds of real-time interactions with your website/app (e.g., live-news coverage).
    

(Video content)

### How to use the `useQuerySubscription` hook

The [`react-datocms`](https://github.com/datocms/react-datocms#live-real-time-updates) package exposes a `useQuerySubscription` hook that uses our [Real-time Updates API](/docs/real-time-updates-api.md) to make any Next.js page update in real-time.

We'll start with the following example, and modify it to **activate real-time updates for any visitor** of your website:

```jsx
const PAGE_CONTENT_QUERY = `{
  allBlogPosts { id title }
  site: _site {
    favicon: faviconMetaTags { attributes content tag }
  }
}`;

export default async function Page() {
  const data = await performRequest(PAGE_CONTENT_QUERY);

  return <LatestBlogPosts data={data} />
}
```

The first step is to build a `<RealtimeLatestBlogPosts />` Client component, utilizing the `useQuerySubscription` hook:

```jsx
'use client';

import { useQuerySubscription } from 'react-datocms';

function RealtimeLatestBlogPosts({ subscription }) {
  const { data, error, status } = useQuerySubscription(subscription);

  return <LatestBlogPosts data={data} error={error} status={status} />;
}
```

Then, in our page component, we can replace the `<LatestBlogPosts />` component with `<RealtimeLatestBlogPosts />`:

```jsx
export default async function Page() {
  const data = await fetchContent();

  return (
    <RealtimeLatestBlogPosts
      subscription={{
        query: PAGE_CONTENT_QUERY,
        initialData: data,
        token: process.env.NEXT_DATOCMS_API_TOKEN,
      }}
    />
  );
}
```

### Draft Mode + `useQuerySubscription`

Perhaps a more common scenario is activating real-time updates not for every visitor, **but only for content editors** in [Draft Mode](/docs/next-js/setting-up-next-js-draft-mode.md), and also showing records in draft:

(Video content)

In this case, the page component will change a bit, as we need to check draft mode activation and either render `<RealtimeLatestBlogPosts />` or `<LatestBlogPosts />`:

```jsx
function fetchContent({ includeDrafts }) {
  return ;
}

export default async function Page() {
  const { isEnabled } = draftMode();

  const data = await performRequest(PAGE_CONTENT_QUERY, { includeDrafts: isEnabled });

  if (isEnabled) {
    return (
      <RealtimeLatestBlogPosts
        subscription={{
          query: PAGE_CONTENT_QUERY,
          initialData: data,
          environment: process.env.NEXT_DATOCMS_ENVIRONMENT,
          token: process.env.NEXT_DATOCMS_API_TOKEN,
        }}
      />
    );
  }

  return <LatestBlogPosts data={data} />
}
```

In summary, the pattern to follow on every page is this:

1.  Do not place the actual content of the page directly inside the `Page` component, but in a secondary component (ie. `<Content />`);
    
2.  Create a real-time wrapper component (ie. `<Realtime />`) that utilizes the `useQuerySubscription` hook, and then renders the `<Content />`;
    
3.  Create the actual Page component and have it return either `<Realtime />`, or `<Content />` based on whether draft mode is active or not.
    

### DRYing everything up

Repeating this pattern for each page can become repetitive and prone to errors, but it is possible to make the code extremely compact and DRY (Don't Repeat Yourself) by using helper functions that generate both the `<Page />` and `<Realtime />` components for you. This way, you can focus solely on the `<Content />` component, which is what actually contains the content of your page.

To see an example of these helper functions, we recommend you take a look at the code of one of our Next.js Starter Kit — for instance, [this is a page component](https://github.com/datocms/nextjs-starter-kit/blob/main/src/app/\(base-layout\)/real-time-updates/page.tsx), [this is a real-time component](https://github.com/datocms/nextjs-starter-kit/blob/main/src/app/\(base-layout\)/real-time-updates/RealTime.tsx), while [this is the actual content](https://github.com/datocms/nextjs-starter-kit/blob/main/src/app/\(base-layout\)/real-time-updates/Content.tsx) — but of course, you can customize them as you prefer to best fit them into your project.

If, however, you want to directly see the end result and the experience for editors, we recommend launching the starter from here:

Next.js Starter Kit

(Image content)

Next.js Starter Kit

Publish this demo online with just three clicks in a matter of minutes.

[Deploy the demo project](https://dashboard.datocms.com/deploy?repo=datocms/nextjs-starter-kit:main) (Image content)

---

# Next.js — DatoCMS Cache Tags and Next.js

Source [docs]: https://www.datocms.com/docs/next-js/using-cache-tags.md

Using [Next.js Cache Tags](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag), you can build pages that respond as pre-rendered content, with the ability to invalidate them later, when the data changes. The idea itself is rather powerful, but as it often happens in computer science, the challenge isn't so much with caching but more about knowing when to invalidate that cache. This is where things get tricky.

Fortunately, we have a solution: [DatoCMS Cache Tags](/docs/content-delivery-api/cache-tags.md) have been designed to **simplify the notoriously difficult problem of caching for developers!**

## **Preamble:** How do Next.js cache tags work?

This diagram provides a summary of the essential steps for understanding [On-Demand Revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#on-demand-revalidation) in Next.js through cache tags:

(Image content)

When the browser requests a page, Next.js by default, responds with a `Cache-Control: public, max-age=0, must-revalidate` header. This tells the browser to always verify from the server if a newer version of the page is available. If there's no change, the server responds with the status [304](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304), therefore saving bandwidth and time. This is referred to as the "revalidate" pattern.

The first time the browser requests a page, both the **Full route cache** and the **Data cache** will be empty, resulting in two `MISS` answers that trigger the `fetch()` requests contained in your routes. The results of those `fetch()` calls will be stored and tagged in Next.js Data Cache. After that, the entire page will be stored in the Next.js Full Route Cache, and marked with the same set of cache tags.

Once the cache has been created, Next.js will be able to answer the following requests with the pre-rendered result and no execution of code, until a [`revalidateTag()`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag) is invoked (for instance, due to a route handler connected to a webhook). In this case, the cache will be cleared and the process will restart from the beginning.

## How to implement DatoCMS Cache Tags with Next.js

Given that Next.js implements cache tags, and DatoCMS provides cache tags... well, the first strategy that comes to mind is to use the Cache Tags of DatoCMS directly as the [`next.tags` option](https://nextjs.org/docs/app/building-your-application/caching#fetch-optionsnexttags-and-revalidatetag) in the `fetch()` calls of your own Next.js project, right?

Unfortunately, this is not possible, because [Next.js can only associate up to a maximum of 128 tags for each `fetch()` request](https://nextjs.org/docs/app/api-reference/functions/fetch#optionsnexttagshttps://nextjs.org/docs/app/api-reference/functions/fetch#optionsnexttags), while DatoCMS can return more than 128 tags per query.

To circumvent the problem, there is an alternative solution, which however requires the use of some type of persistent database. Great options are [Turso](https://turso.tech/) or [Vercel Postgres](https://vercel.com/docs/storage/vercel-postgres).

### The idea

Before we delve into the details, let's focus on the pattern we're aiming for:

-   Implement a function — i.e., `executeQuery()` — responsible for executing a GraphQL query using the DatoCMS Content Delivery API, and caching the result.
    
    1.  To be able to invalidate this request later, the `fetch()` needs to tag the request. We'll use a single tag and call it "Query ID", as it will be unique for each query.
        
    2.  Before returning the result of the query, `executeQuery()` needs to read the `X-Cache-Tags` header in the response, and save the "Query ID to DatoCMS Cache Tags" mappings in the DB.
        
-   Implement a route handler listening for ["Cache Tag Invalidation" events](/docs/content-delivery-api/cache-tags.md#step-3-implement-the-invalidate-cache-tag-webhook). The route needs to:
    
    1.  Take from the webhook payload the DatoCMS Cache Tags that need to be invalidated;
        
    2.  Search the DB for all the Query IDs linked to these cache tags;
        
    3.  Use [`revalidateTag()`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag) to invalidate all the identified Query IDs.
        
-   In each route that uses `executeQuery()`, set up `dynamic = 'force-static'` as [Route Segment Config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config).
    

### The actual code

We have prepared a [Next.js project perfectly configured to integrate with DatoCMS Cache Tags](https://github.com/datocms/nextjs-with-cache-tags-starter). Every part of the code is thoroughly commented to assist you in understanding.

We recommend starting from the ["Useful resources to navigate the code"](https://github.com/datocms/nextjs-with-cache-tags-starter?tab=readme-ov-file#useful-resources-to-navigate-the-code) section of the README for a general overview, and links to the most important parts of the code in the repo.

---

# Next.js — Visual Editing

Source [docs]: https://www.datocms.com/docs/next-js/visual-editing.md

Visual Editing represents the ultimate content management experience — the "holy grail" for content editors. Instead of navigating through forms and fields in a CMS interface, editors can see their content exactly as it appears on the live site, click directly on any element to edit it, and watch changes appear instantly.

This seamless experience is achieved by combining several techniques that work together:

1.  [**Draft Mode**](/docs/next-js/setting-up-next-js-draft-mode.md) — Access unpublished content during preview sessions
    
2.  [**Real-time Updates**](/docs/next-js/real-time-updates.md) — See content changes reflected immediately without page refresh
    
3.  **Content Link** — Click-to-edit overlays that connect frontend elements to their CMS fields
    
4.  [**Web Previews Plugin**](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) — The DatoCMS plugin that orchestrates the editing experience
    

This guide focuses on **Content Link** and **Web Previews** — the final pieces that transform a preview into a true visual editing environment.

## Two levels of integration

Visual Editing can be set up incrementally:

##### Level 1: Content Link (standalone)

With just Content Link configured, editors browsing your website in draft mode can click on any content element to edit it. **Clicking opens DatoCMS in a new browser tab**, navigating directly to the field that controls that content.

This works entirely on your website — no DatoCMS plugin required. It's a great starting point that already provides significant value to editors.

(Video content)

Click-to-edit overlays

##### Level 2: Web Previews Plugin (side-by-side)

Adding the Web Previews plugin takes it further: editors can now **view the website and DatoCMS interface side-by-side within DatoCMS itself**. When they click on content, the edit panel opens instantly in the same view — no tab switching required.

The plugin also enables:

-   Preview links in the DatoCMS sidebar
-   Bidirectional navigation (browse the preview, and DatoCMS follows along)
    
-   Full-screen Visual Editing mode
    

(Video content)

Side-by-side editing

## Content Link: Click-to-edit overlays

Content Link enables the "click-to-edit" functionality by embedding invisible metadata (called "stega encoding") into your content. When editors hover over content in draft mode, visual overlays appear indicating which elements are editable.

**This works entirely on your website** — editors simply browse the site in draft mode, and clicking any editable element opens DatoCMS in a new tab. No plugin installation required.

(Image content)

Content Link overlays

##### How it works

1.  **Stega encoding** — When fetching draft content, pass the `contentLink` and `baseEditingUrl` options to embed invisible metadata into text fields
    
2.  **Detection** — The `<ContentLink />` component scans your page for this encoded content
    
3.  **Overlays** — Interactive overlays appear when editors hover over editable content
    
4.  **Deep linking** — Clicking an element opens DatoCMS at the exact field that controls that content
    

##### Setting up Content Link

The setup involves two parts:

**Enable stega encoding** when fetching draft content by passing the `contentLink` and `baseEditingUrl` options (see [`src/lib/datocms/executeQuery.ts`](https://github.com/datocms/nextjs-starter-kit/blob/main/src/lib/datocms/executeQuery.ts) for the full implementation):

```jsx
performRequest(query, {
  includeDrafts: true,
  contentLink: 'v1',
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
```

**Add the** **`<ContentLink />`** **component** to your root layout, rendered only in draft mode (see [`src/components/ContentLink`](https://github.com/datocms/nextjs-starter-kit/blob/main/src/components/ContentLink/index.tsx) and [`src/app/layout.tsx`](https://github.com/datocms/nextjs-starter-kit/blob/main/src/app/layout.tsx) for implementation details):

```jsx
{isDraftModeEnabled && <ContentLink />}
```

For more advanced use cases — like building a custom toolbar to toggle edit mode, or programmatically triggering the "flash all editable elements" animation — use the [`useContentLink` hook](https://github.com/datocms/react-datocms/blob/master/docs/content-link.md#advanced-usage-the-usecontentlink-hook):

```jsx
const { enableClickToEdit, disableClickToEdit, flashAll } = useContentLink();
```

For component props and keyboard shortcuts, see the [react-datocms ContentLink documentation](https://github.com/datocms/react-datocms/blob/master/docs/content-link.md#props).

##### Working with Structured Text

Structured Text fields require two rules for Visual Editing to work correctly.

**Rule 1: Always wrap the Structured Text component in a group.** This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```jsx
<div data-datocms-content-link-group>
  <StructuredText data={content.body} />
</div>
```

**Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary.** These elements have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Note that `renderLinkToRecord` does **not** need a boundary — record links are just `<a>` tags wrapping text that belongs to the surrounding structured text, so there's no URL collision.

```jsx
<div data-datocms-content-link-group>
  <StructuredText
    data={page.content}
    renderBlock={({ record }) => (
      <div data-datocms-content-link-boundary>
        <BlockComponent block={record} />
      </div>
    )}
    renderInlineRecord={({ record }) => (
      <span data-datocms-content-link-boundary>
        <InlineRecordComponent record={record} />
      </span>
    )}
    renderLinkToRecord={({ record, children, transformedMeta }) => (
      <a {...transformedMeta} href={`/resources/${record.slug}`}>
        {children}
      </a>
    )}
    renderInlineBlock={({ record }) => (
      <span data-datocms-content-link-boundary>
        <InlineBlockComponent record={record} />
      </span>
    )}
  />
</div>
```

See the [ContentLink Structured Text documentation](https://github.com/datocms/react-datocms/blob/master/docs/content-link.md#structured-text-fields) for details.

## Web Previews Plugin (optional enhancement)

The [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) plugin enhances the editing experience by embedding your website preview directly inside DatoCMS. Instead of switching between browser tabs, editors get a **side-by-side view** where clicking on content instantly opens the edit panel.

When Content Link detects it's running inside the Web Previews plugin iframe, it automatically switches from opening new tabs to communicating with the plugin — no code changes required.

(Image content)

Side-by-side editing in DatoCMS

##### How it works

The plugin communicates with your frontend through two API endpoints:

1.  **Preview Links API** — Receives record info from DatoCMS and returns preview URLs (see [`src/app/api/preview-links/route.tsx`](https://github.com/datocms/nextjs-starter-kit/blob/main/src/app/api/preview-links/route.tsx) for the full implementation):
    

app/api/preview-links/route.js

```jsx
export async function POST(request) {
  const { item, locale } = await request.json();
  const url = await recordToWebsiteRoute(item, locale);

  return NextResponse.json({
    previewLinks: [{ label: 'Draft', url: `/api/draft-mode/enable?redirect=${url}` }]
  });
}
```

1.  **Enable Draft Mode route** — Activates draft mode and redirects to the preview. This is the same route [covered in the Draft Mode guide](/docs/next-js/setting-up-next-js-draft-mode.md).
    

##### Configuring the plugin

In your DatoCMS project:

1.  Install the [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) from the marketplace
    
2.  Configure a frontend with:
    
    -   **Preview Links API endpoint**: `https://yoursite.com/api/preview-links?token=your-secret`
        
    -   **Enable Draft Mode route**: `https://yoursite.com/api/draft-mode/enable?token=your-secret`
        

For full configuration details, see the [Web Previews plugin documentation](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md#installation-and-configuration).

> [!WARNING] Content Security Policy
> If your website implements a Content Security Policy with a `frame-ancestors` directive, you need to allow the DatoCMS plugin to embed your site. In Next.js, this is typically configured via the `headers` option in `next.config.js`:
> 
> next.config.js
> 
> ```jsx
> headers: async () => [{
>   source: '/:path*',
>   headers: [{
>     key: 'Content-Security-Policy',
>     value: "frame-ancestors 'self' https://plugins-cdn.datocms.com"
>   }]
> }]
> ```

---

# Nuxt — Nuxt + DatoCMS Overview

Source [docs]: https://www.datocms.com/docs/nuxt.md

Nuxt is an approachable tool for building projects based on Vue.js. It includes file-system routing, minimal configuration, and a set of meaningful conventions: it's the right tool to start a Vue.js application without reinventing the wheel each time.

DatoCMS is the perfect companion to Nuxt since it offers content, images and videos on a globally-distributed CDN. With this combo, you can have an **infinitely scalable website, ready to handle prime-time TV traffic spikes, at a fraction of the regular cost.**

> [!PROTIP] Pro tip: Build a Blog With Nuxt and DatoCMS
> For a step-by-step tutorial on integrating DatoCMS into a Nuxt blog, [check out this guide](https://www.datocms.com/blog/how-to-build-a-nuxt-blog.md). It covers creating content models, adding and retrieving blog posts, handling dynamic routing, and offers tips on styling your blog for a polished look.

Our [marketplace](https://www.datocms.com/marketplace/starters.md) features different demo projects on Nuxt, so you can learn and get started easily:

[

(Image content)

Nuxt Starter Kit

Try this demo »

](https://www.datocms.com/marketplace/starters/nuxt-starter-kit.md)

### Fetching contents from our GraphQL API

First, create a new Nuxt application, which sets up a basic Nuxt application for you. To create a project, run the following command:

Terminal window

```bash
npx nuxi init nuxt-app
```

Then enter inside the project directory, install the dependencies, and start the development server:

Terminal window

```bash
cd nuxt-app
npm run dev
```

We also need the [`@datocms/cda-client` package](https://github.com/datocms/cda-client), which provides a series of convenient utilities for making calls to the Content Delivery API:

Terminal window

```bash
npm i --save @datocms/cda-client
```

Nuxt comes with a [set of methods](https://v3.nuxtjs.org/getting-started/data-fetching) for fetching data from any API. The best way to retrieve data from Dato's GraphQL API is building a custom composable relying on `useFetch`:

```javascript
import { buildRequestInit } from '@datocms/cda-client';

export function useQuery(query, options) {
  const config = useRuntimeConfig();

  const optionsWithToken = {
    ...options,
    token: config.datocmsApiToken,
  };

  return useFetch('https://graphql.datocms.com/', {
    ...buildRequestInit(query, optionsWithToken),
    key: hash([query, optionsWithToken]),
    transform: ({ data, errors }) => {
      if (errors)
        throw new Error(
          `Something went wrong while executing the query: ${JSON.stringify(errors)}`,
        );

      return data;
    },
  });
}
```

The DatoCMS API token can be stored in an [environment variable](https://v3.nuxtjs.org/getting-started/configuration#environment-variables-and-private-tokens) and provided to Nuxt application via the `nuxt.config.ts` file:

```javascript
export default defineNuxtConfig({
  runtimeConfig: {
    // set by NUXT_DATOCMS_API_TOKEN env variable
    datocmsApiToken: '',
  }
})
```

To create an API token for a DatoCMS project, go in the "Settings \> API Tokens" section, making sure you only give it permission to access the (read-only) Content Delivery API.

(Video content)

Finally, you'll need to set up a `.env` file to store the DatoCMS token:

```plaintext
DATO_CMS_TOKEN=<THE_TOKEN_YOU_JUST_CREATED>
```

You can then use the composable in your pages and layouts:

```javascript
<script setup>
const QUERY = `
  query {
    blog {
      title
    }
  }
`;

const { data, error } = useQuery(QUERY);
</script>

<template>
  <p v-if="error">Something bad happened!</p>
  <p v-else>Data: <code>{{ JSON.stringify(data) }}</code></p>
</template>
```

The `QUERY` is the GraphQL query, and of course, it depends on the models available in your specific DatoCMS project. You can learn everything you need regarding how to build GraphQL queries on our [Content Delivery API documentation](/docs/content-delivery-api.md).

---

# Nuxt — Include draft contents

Source [docs]: https://www.datocms.com/docs/nuxt/include-draft-contents-during-development.md

While you're working on a Nuxt website, it may be useful to include draft contents from DatoCMS: this way, you can preview how the site will look in the end before actually publishing any record.

To do that, you need to tell our GraphQL API to include draft records when executing the queries. The `X-Include-Drafts` is one of many headers you can use to shape up the behavior of the Content Delivery API. Check out the other [available headers in the Content Delivery API](/docs/content-delivery-api/api-endpoints.md).

If you want a preview of the contents while working on the site in development mode, we can do as follow.

First, change the `nuxt.config.ts` file to expose the current environment:

```javascript
// In the nuxt.config.ts

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      env: process.env.NODE_ENV
    }
  }
})
```

Then, in the pages you can check the environment to decide to include draft records or not:

```javascript
<script setup>
const QUERY = `
  {
    blog { title }
  }
`;

const config = useRuntimeConfig()

const { data, error } = await useQuery(QUERY, {
  includeDrafts: config.env !== 'production'
});
</script>
```

---

# Nuxt — Responsive images

Source [docs]: https://www.datocms.com/docs/nuxt/managing-images.md

One of the advantages of using DatoCMS is its [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images), which will return **pre-computed image attributes that will help you setting up responsive images in your frontend without any additional manipulation**.

To make it even easier to use, we offer a Vue component ready to use to render responsive, progressive images on your projects. The package called [`vue-datocms`](https://github.com/datocms/vue-datocms) exposes an `<Image />` component and pairs perfectly with the `responsiveImage` query.

Our solution offers similar advantages of using [NuxtImage](https://image.nuxtjs.org/), with the benefit of having beautiful low-quality image placeholders (LQIP) in base64 format embedded directly within the page and responsive images optimized for the user browser and resolution. Images are managed directly via the DatoCMS Media section:

(Video content)

To take advantage of it, install the [`vue-datocms`](https://github.com/datocms/vue-datocms) package:

Terminal window

```bash
yarn add vue-datocms
```

Then, inside your Nuxt page, feed content coming from a [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images) directly into the `<Image />` component:

```html
<script setup>
import { Image as DatocmsImage } from "vue-datocms";

const QUERY = `query HomePage($limit: IntType) {
  allBlogPosts(first: $limit) {
    id
    title
    coverImage {
      responsiveImage(imgixParams: { fit: crop, w: 300, h: 300, auto: format }) {
        srcSet
        webpSrcSet
        sizes
        src
        width
        height
        aspectRatio
        alt
        title
        base64
      }
    }
  }
}`;

const { data, error } = await useQuery(QUERY);
</script>

<template>
  <div>
    <article v-for="blogPost in data.allBlogPosts" :key="blogPost.id">
      <DatocmsImage :data="blogPost.coverImage.responsiveImage" />
      <h1>{{ blogPost.title }}</h1>
    </article>
  </div>
</template>
```

The `vue-datocms` package also offer a `<NakedImage />` component which generates minimum JS footprint, outputs a single `<picture />` element and implements lazy-loading using the native [`loading="lazy"` attribute](https://web.dev/articles/browser-level-image-lazy-loading). You can refer to the package [README](https://github.com/datocms/vue-datocms/tree/master/src/components/Image) to learn more.

---

# Nuxt — Displaying videos

Source [docs]: https://www.datocms.com/docs/nuxt/displaying-videos.md

> [!PROTIP] Pro tip: Start with our how-to guides first
> If you're new to hosting videos on DatoCMS, we recommend first starting with our tutorials:
> 
> -   How to upload videos: [Videos and Video Optimizations](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)
>     
> -   Why you should use HLS Streaming via Mux: [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md)
>     
> 
> Then, this page provides framework-specific playback advice using our helper components. Read on when you're ready!

One of the advantages of using DatoCMS instead of other content management systems is its `video` query, which will return **pre-computed video attributes that will help you display videos in your frontend without any additional manipulation**.

To make it easy to offer optimized, progressive videos on your projects, we offer a package called [`vue-datocms`](https://github.com/datocms/vue-datocms) that exposes a `<VideoPlayer />` component and pairs perfectly with the video query.

To take advantage of it, install the vue-datocms package:

Terminal window

```bash
npm install vue-datocms
```

Then, inside your page, feed content coming from a `video` query directly into the `<VideoPlayer />` component:

```html
<script setup>
import { VideoPlayer } from "vue-datocms";

const QUERY = `query HomePage($limit: IntType) {
  allBlogPosts(first: $limit) {
    id
    title
    coverVideo {
      video {
        muxPlaybackId
        title
        width
        height
        blurUpThumb
      }
    }
  }
}`;

const { data } = await useQuery(QUERY);
</script>

<template>
  <div v-if="data">
    <article v-for="blogPost of data.allBlogPosts" v-bind:key="blogPost.id">
      <h6>{{blogPost.title}}</h6>
      <VideoPlayer :data="blogPost.coverVideo.video" />
    </article>
  </div>
</template>
```

---

# Nuxt — Structured Text fields

Source [docs]: https://www.datocms.com/docs/nuxt/rendering-structured-text-fields.md

Rich text in DatoCMS is stored in [Structured Text](/docs/content-modelling/structured-text.md) fields, which lets us use it in many different contexts, from HTML in the browser to speech fulfillments in voice interfaces, if that's what you want.

There's a lot to be said about Structured Text and the extensibility of it, but for now let's just say that it returns content in a particular [JSON format called `dast`](/docs/structured-text/dast.md) which will resemble this example:

```json
{
  "schema": "dast",
  "document": {
    "type": "root",
    "children": [
      {
        "type": "heading",
        "level": 1,
        "children": [
          {
            "type": "span",
            "marks": [],
            "value": "Hello world!"
          }
        ]
      }
    ]
  }
}
```

To make it easy to convert this format in HTML inside your Nuxt projects, we provide a package called [`vue-datocms`](https://github.com/datocms/vue-datocms) that exposes a `<StructuredText />` component that does all the tedious work for you.

To take advantage of it, install the [`vue-datocms`](https://github.com/datocms/vue-datocms) package if you haven't already:

Terminal window

```bash
yarn add vue-datocms
```

Then, inside your page, make a [GraphQL query to fetch a Structured Text field](/docs/content-delivery-api/structured-text-fields.md), and feed the result to the `data` prop of a `<StructuredText />` component:

```html
<script setup>
import { StructuredText as DatocmsStructuredText } from "vue-datocms";

const QUERY = `query HomePage($limit: IntType) {
  allBlogPosts(first: $limit) {
    id
    title
    content {
      value
    }
  }
}`;

const { data } = await useQuery(QUERY);
</script>

<template>
  <div>
    <article v-for="blogPost in data.allBlogPosts" :key="blogPost.id">
      <h1>{{ blogPost.title }}</h1>
      <DatocmsStructuredText :data="blogPost.content" />
    </article>
  </div>
</template>
```

## Rendering special nodes

Other than "regular" formatting nodes (paragraphs, lists, etc.), Structured Text documents can contain four particular types of nodes:

-   [`itemLink` nodes](/docs/structured-text/dast.md#itemLink) are just like regular HTML hyperlinks, but point to other records instead of URLs;
-   [`inlineItem` nodes](/docs/structured-text/dast.md#inlineItem) lets you directly embed a reference to a record in-between regular text;
    
-   [`block` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular paragraphs;
-   [`inlineBlock` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular text;
    

If a Structured Text document contains one of these nodes, then we need to change the GraphQL query, so that we also fetch all the records and blocks it references. As an example, if the field can link to other Blog posts, and can embed blocks of type "Image block" and "Mention block", then the query should change like this:

```jsx
const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  allBlogPosts(first: $limit) {
    id
    title
    content {
      value
      blocks {
        __typename
        ... on ImageBlockRecord {
          id
          image { url alt }
        }
      }
      inlineBlocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on MentionBlockRecord {
          username
        }
      }
      links {
        __typename
        ... on BlogPostRecord {
          id
          slug
          title
        }
      }
    }
  }
}`;
```

We also need to tell `<StructuredText />` how you want such nodes to be rendered:

```html
<script setup>
import { h } from 'vue'

const renderInlineRecord = ({ record }) => {
  if (record.__typename === 'BlogPostRecord') {
    return h('a', { href: `/blog/${record.slug}` }, [record.title]);
  }
  return null;
};

const renderLinkToRecord = ({ record, children }) => {
  if (record.__typename === 'BlogPostRecord') {
    return h('a', { href: `/blog/${record.slug}` }, children);
  }
  return null;
};

const renderBlock = ({ record, key }) => {
  if (record.__typename === 'ImageBlockRecord') {
    return h(DatocmsImage, { key, props: { data: record.image.responsiveImage } });
  }
  return null;
};

const renderInlineBlock = ({ record, key }) => {
  if (record.__typename === 'MentionBlockRecord') {
    return h('code', { key }, `@${record.username}`);
  }
  return null;
};

// ...
</script>

<template>
  <div>
    <article v-for="blogPost of data.allBlogPosts" :key="blogPost.id">
      <h1>{{ blogPost.title }}</h1>
      <datocms-structured-text
        :data="blogPost.content"
        :render-inline-record="renderInlineRecord"
        :render-link-to-record="renderLinkToRecord"
        :render-block="renderBlock"
        :render-inline-block="renderInlineBlock"
      />
    </article>
  </div>
</template>
```

---

# Nuxt — Adding SEO to Nuxt pages

Source [docs]: https://www.datocms.com/docs/nuxt/seo-management.md

Similarly to what we offer with [responsive images](/docs/nuxt/managing-images.md), our GraphQL API also offers a way to fetch [**pre-computed SEO meta tags**](/docs/content-delivery-api/seo-and-favicon.md) **based on the content you insert inside DatoCMS**.

You can easily use this information inside your Nuxt app with the help of our [`vue-datocms`](https://github.com/datocms/vue-datocms) package.

Here's a sample of the meta tags you can automatically generate:

```html
<title>DatoCMS Blog - DatoCMS</title>
<meta property="og:title" content="DatoCMS Blog" />
<meta name="twitter:title" content="DatoCMS Blog" />
<meta name="description" content="Lorem ipsum..." />
<meta property="og:description" content="Lorem ipsum..." />
<meta name="twitter:description" content="Lorem ipsum..." />
<meta property="og:image" content="https://www.datocms-assets.com/..." />
<meta property="og:image:width" content="2482" />
<meta property="og:image:height" content="1572" />
<meta name="twitter:image" content="https://www.datocms-assets.com/..." />
<meta property="og:locale" content="en" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DatoCMS" />
<meta property="article:modified_time" content="2020-03-06T15:07:14Z" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@datocms" />
<link sizes="16x16" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="32x32" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="96x96" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="192x192" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
```

To do that, install the [`vue-datocms`](https://github.com/datocms/vue-datocms) package:

Terminal window

```bash
yarn add vue-datocms
```

Then, inside your page, feed content coming from a `faviconMetaTags` or `_seoMetaTags` query into the `toHead` function and combine that with the [`useHead`](https://v3.nuxtjs.org/api/composables/use-head) composable:

```html
<script setup>
import { toHead } from "vue-datocms";

const QUERY = `query {
  site: _site {
    favicon: faviconMetaTags {
      attributes
      content
      tag
    }
  }
  blog {
    seo: _seoMetaTags {
      attributes
      content
      tag
    }
  }
}`;

const { data } = await useQuery(QUERY);

useHead(() => {
  if (!data.value) return {}

  return toHead(data.value.blog.seo, data.value.site.favicon)
})
</script>
```

Want to know more about SEO customization in DatoCMS? Check out this video tutorial:

[

(Image content)

Working with and customizing SEO Fields

Play video »

](https://youtu.be/WjF10isSjS0)

---

# Nuxt — Real-time updates

Source [docs]: https://www.datocms.com/docs/nuxt/real-time-updates.md

Live updates are useful both for content editors and the regular visitors of your app/website:

-   Content-editors in can **see drafts directly in the website**, without having to refresh the page;
-   Visitors can **immediately see new content as it gets published**, allowing all kinds of real-time interactions with your website/app (ie. live-news coverage).
    

(Video content)

Nuxt and [`vue-datocms`](https://github.com/datocms/vue-datocms) together make it easy to use our [Real-time Updates API](/docs/real-time-updates-api.md) to perform such changes, as it only involves adding a composable to your pages.

### How to use the `useQuerySubscription` composable

The [`vue-datocms`](https://github.com/datocms/vue-datocms) package exposes a [`useQuerySubscription`](https://github.com/datocms/vue-datocms/tree/master/src/composables/useQuerySubscription) function that makes it trivial to make any Nuxt page updated in real-time. The composable works by streaming any changes to the GraphQL response to the browser.

The following code shows a complete example that **activates real-time updates for any visitor** of your website:

```html
<script setup>
import { useQuerySubscription } from "vue-datocms";

const statusMessage = {
  connecting: 'Connecting to DatoCMS...',
  connected: 'Connected to DatoCMS, receiving live updates!',
  closed: 'Connection closed',
};

const runtimeConfig = useRuntimeConfig();

const QUERY = `
  query {
    blogPost {
      title
    }
  }
`;

const { status, error, data } = useQuerySubscription({
  query: QUERY,
  token: config.datocmsApiToken
});
</script>

<template>
  <div>
    <p>Connection status: {{ statusMessage[status] }}</p>
    <div v-if="error">
      <h1>Error: {{ error.code }}</h1>
      <div>{{ error.message }}</div>
      <pre v-if="error.response">{{ JSON.stringify(error.response, null, 2) }}</pre>
    </div>
    <div v-if="data">{{ JSON.stringify(data, null, 2) }}</div>
  </div>
</template>
```

---

# Nuxt — Visual Editing

Source [docs]: https://www.datocms.com/docs/nuxt/visual-editing.md

Visual Editing represents the ultimate content management experience — the "holy grail" for content editors. Instead of navigating through forms and fields in a CMS interface, editors can see their content exactly as it appears on the live site, click directly on any element to edit it, and watch changes appear instantly.

This seamless experience is achieved by combining several techniques that work together:

1.  [**Draft Mode**](/docs/nuxt/include-draft-contents-during-development.md) — Access unpublished content during preview sessions
    
2.  [**Real-time Updates**](/docs/nuxt/real-time-updates.md) — See content changes reflected immediately without page refresh
    
3.  **Content Link** — Click-to-edit overlays that connect frontend elements to their CMS fields
    
4.  [**Web Previews Plugin**](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) — The DatoCMS plugin that orchestrates the editing experience
    

This guide focuses on **Content Link** and **Web Previews** — the final pieces that transform a preview into a true visual editing environment.

## Two levels of integration

Visual Editing can be set up incrementally:

##### Level 1: Content Link (standalone)

With just Content Link configured, editors browsing your website in draft mode can click on any content element to edit it. **Clicking opens DatoCMS in a new browser tab**, navigating directly to the field that controls that content.

This works entirely on your website — no DatoCMS plugin required. It's a great starting point that already provides significant value to editors.

(Video content)

Click-to-edit overlays

##### Level 2: Web Previews Plugin (side-by-side)

Adding the Web Previews plugin takes it further: editors can now **view the website and DatoCMS interface side-by-side within DatoCMS itself**. When they click on content, the edit panel opens instantly in the same view — no tab switching required.

The plugin also enables:

-   Preview links in the DatoCMS sidebar
-   Bidirectional navigation (browse the preview, and DatoCMS follows along)
    
-   Full-screen Visual Editing mode
    

(Video content)

Side-by-side editing

## Content Link: Click-to-edit overlays

Content Link enables the "click-to-edit" functionality by embedding invisible metadata (called "stega encoding") into your content. When editors hover over content in draft mode, visual overlays appear indicating which elements are editable.

**This works entirely on your website** — editors simply browse the site in draft mode, and clicking any editable element opens DatoCMS in a new tab. No plugin installation required.

(Image content)

Content Link overlays

##### How it works

1.  **Stega encoding** — When fetching draft content, pass the `contentLink` and `baseEditingUrl` options to embed invisible metadata into text fields
    
2.  **Detection** — The `<ContentLink />` component scans your page for this encoded content
    
3.  **Overlays** — Interactive overlays appear when editors hover over editable content
    
4.  **Deep linking** — Clicking an element opens DatoCMS at the exact field that controls that content
    

##### Setting up Content Link

The setup involves two parts:

**Enable stega encoding** when fetching draft content by passing the `contentLink` and `baseEditingUrl` options (see [`composables/useQuery.ts`](https://github.com/datocms/nuxt-starter-kit/blob/main/composables/useQuery.ts) for the full implementation):

```javascript
import { buildRequestInit } from '@datocms/cda-client';

useFetch('https://graphql.datocms.com/', {
  ...buildRequestInit(query, {
    token: apiToken,
    includeDrafts: true,
    contentLink: 'v1',
    baseEditingUrl: 'https://your-project.admin.datocms.com',
  }),
});
```

**Add the** **`<ContentLink />`** **component** to your root layout, rendered only in draft mode (see [`components/ContentLink`](https://github.com/datocms/nuxt-starter-kit/blob/main/components/ContentLink/index.vue) and [`app.vue`](https://github.com/datocms/nuxt-starter-kit/blob/main/app.vue) for implementation details):

```html
<template>
  <ClientOnly>
    <ContentLink v-if="draftMode" />
  </ClientOnly>
</template>
```

For more advanced use cases — like building a custom toolbar to toggle edit mode, or programmatically triggering the "flash all editable elements" animation — use the [`useContentLink` composable](https://github.com/datocms/vue-datocms/blob/master/src/components/ContentLink/README.md#advanced-usage-the-usecontentlink-composable):

```javascript
import { useContentLink } from 'vue-datocms';

const { enableClickToEdit, disableClickToEdit, flashAll } = useContentLink();
```

For component props and keyboard shortcuts, see the [vue-datocms ContentLink documentation](https://github.com/datocms/vue-datocms/blob/master/src/components/ContentLink/README.md#props).

##### Working with Structured Text

Structured Text fields require two rules for Visual Editing to work correctly.

**Rule 1: Always wrap the Structured Text component in a group.** This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```html
<template>
  <div data-datocms-content-link-group>
    <StructuredText :data="content.body" />
  </div>
</template>
```

**Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary.** These elements have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Note that `renderLinkToRecord` does **not** need a boundary — record links are just `<a>` tags wrapping text that belongs to the surrounding structured text, so there's no URL collision.

```javascript
import { h } from 'vue';

const renderBlock = ({ record }) => {
  return h('div', { 'data-datocms-content-link-boundary': '' }, [
    h(ImageBlockComponent, { block: record })
  ]);
};

const renderInlineRecord = ({ record }) => {
  return h('a', { href: `/team/${record.slug}`, 'data-datocms-content-link-boundary': '' }, record.firstName);
};

const renderLinkToRecord = ({ record, children, transformedMeta }) => {
  return h('a', { ...transformedMeta, href: `/team/${record.slug}` }, children);
};

const renderInlineBlock = ({ record }) => {
  return h('code', { 'data-datocms-content-link-boundary': '' }, `@${record.username}`);
};
```

See the [ContentLink Structured Text documentation](https://github.com/datocms/vue-datocms/blob/master/src/components/ContentLink/README.md#structured-text-fields) for details.

## Web Previews Plugin (optional enhancement)

The [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) plugin enhances the editing experience by embedding your website preview directly inside DatoCMS. Instead of switching between browser tabs, editors get a **side-by-side view** where clicking on content instantly opens the edit panel.

When Content Link detects it's running inside the Web Previews plugin iframe, it automatically switches from opening new tabs to communicating with the plugin — no code changes required.

(Image content)

Side-by-side editing in DatoCMS

##### How it works

The plugin communicates with your frontend through two API endpoints:

**Preview Links API** — Receives record info from DatoCMS and returns preview URLs (see [`server/api/preview-links/index.ts`](https://github.com/datocms/nuxt-starter-kit/blob/main/server/api/preview-links/index.ts) for the full implementation):

server/api/preview-links/index.ts

```javascript
export default eventHandler(async (event) => {
  const { item, locale } = await readBody(event);
  const url = recordToWebsiteRoute(item, locale);

  return {
    previewLinks: [{ label: 'Draft', url: `/api/draft-mode/enable?redirect=${url}` }]
  };
});
```

**Enable Draft Mode route** — Activates draft mode and redirects to the preview. This is the same route [covered in the Draft Mode guide](/docs/nuxt/include-draft-contents-during-development.md).

##### Configuring the plugin

In your DatoCMS project:

1.  Install the [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) from the marketplace
    
2.  Configure a frontend with:
    
    -   **Preview Links API endpoint**: `https://yoursite.com/api/preview-links?token=your-secret`
        
    -   **Enable Draft Mode route**: `https://yoursite.com/api/draft-mode/enable?token=your-secret`
        

For full configuration details, see the [Web Previews plugin documentation](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md#installation-and-configuration).

> [!WARNING] Content Security Policy
> If your website implements a Content Security Policy with a `frame-ancestors` directive, you need to allow the DatoCMS plugin to embed your site. In Nuxt, this is typically configured via the `routeRules` option or a server middleware:
> 
> nuxt.config.ts
> 
> ```javascript
> export default defineNuxtConfig({
>   routeRules: {
>     '/**': {
>       headers: {
>         'Content-Security-Policy': "frame-ancestors 'self' https://plugins-cdn.datocms.com"
>       }
>     }
>   }
> });
> ```

---

# SvelteKit — SvelteKit + DatoCMS Overview

Source [docs]: https://www.datocms.com/docs/svelte.md

Svelte is a frontend framework built around a simple idea: avoid the complexity of a Virtual DOM and compile components to responsive vanilla JS. SvelteKit is Svelte's full-stack framework: it sports file-based routing, API endpoints, and zero-configuration deployments on multiple providers (adapters for Vercel, Netlify and Cloudflare are provided and even used transparently for a great developer experience).

Svelte and SvelteKit together let you get started quickly with a set of sane defaults upon which you can build.

DatoCMS is the perfect companion to SvelteKit since it offers content, images and videos on a globally-distributed CDN. With this combo, you can have an **infinitely scalable website, ready to handle prime-time TV traffic spikes at a fraction of the regular cost.**

In the next paragraphs, will see how easy it is to combine Svelte with DatoCMS.

### Fetching content from our GraphQL API

Svelte and SvelteKit invites developers to leverage [existing standard APIs](https://kit.svelte.dev/docs/web-standards). [`fetch` API](https://kit.svelte.dev/docs/web-standards#fetch-apis) is the conventional way of retrieving data from servers.

Let's start by installing `@datocms/cda-client`, a lightweight, TypeScript-ready package that offers various helpers around the native Fetch API to perform GraphQL requests towards [DatoCMS Content Delivery API](/docs/content-delivery-api/api-endpoints.md):

Terminal window

```bash
npm install --save @datocms/cda-client
```

We can now create a function we can use in all of our components that need to fetch content from DatoCMS: Create a new directory called `lib`, and inside of it, add a file called `datocms.js`:

src/lib/datocms.js

```javascript
import { env as privateEnv } from '$env/dynamic/private';
import { executeQuery } from '@datocms/cda-client';

export const performRequest = (query, options) => {
  return executeQuery(query, {
    ...options,
    token: privateEnv.PRIVATE_DATOCMS_CDA_TOKEN,
  });
}
```

Make sure you set `PRIVATE_DATOCMS_CDA_TOKEN` as an actual API token of your DatoCMS project. You can create a new one under "Settings \> API Tokens".

(Video content)

Loading data is achieved in SvelteKit by creating a `+page.server.js` file beside the `+page.svelte` component.

The `load` function exported from `+page.js` is called when the page is loaded. Here we can use our `executeQuery` function to load content from DatoCMS:

src/routes/+page.server.ts

```javascript
const query = `
  query HomeQuery {
    blogPost { title }
  }
`;

export const load = () => {
  return executeQuery(query);
};
```

The data returned by the `load` function will be available to the page/layout component a `data` prop:

src/routes/+page.svelte

```html
<script>
export let data;
</script>

<article>
  <h1>{{ data.blogPost.title }}</h1>
</article>
```

You can learn everything you need regarding how to build GraphQL queries on our [Content Delivery API documentation](/docs/content-delivery-api.md).

---

# SvelteKit — Accessing draft/updated content

Source [docs]: https://www.datocms.com/docs/svelte/accessing-draft-updated-content-with-fetch.md

If you have [draft/published mode](/docs/general-concepts/draft-published.md) enabled on some of your models, you can use [the `X-Include-Drafts` header](/docs/content-delivery-api/api-endpoints.md#include-drafts) to **access records at their latest version available** instead of the currently published one:

Pages and layouts can utilize the `includeDrafts` option of the `executeQuery` function in their server load functions:

src/routes/+page.server.ts

```javascript
const query = `
  query HomeQuery {
    blogPost { title }
  }
`;

export const load = () => {
  return executeQuery(query, { includeDrafts: true });
};
```

The `X-Include-Drafts` is one of many headers you can use to shape up the behavior of the Content Delivery API. Check out the other [available headers in the Content Delivery API](/docs/content-delivery-api/api-endpoints.md).

### Setting up Draft Mode

If your SvelteKit site is deployed as a **dynamic site** (server-side rendered on each request), you can implement a "Draft Mode" toggle that allows content editors to switch between viewing published content and draft content directly on the website.

Unlike Next.js, SvelteKit doesn't have a built-in draft mode feature. However, you can implement it yourself using cookies to store the draft mode state. The basic approach involves:

-   **API routes to enable/disable draft mode** — These set or delete a cookie that indicates whether draft mode is active.
-   **A helper function to check draft mode status** — Used in your `load` functions to determine whether to include drafts in API requests.
    
-   **Conditional query execution** — Pass the `includeDrafts` option based on the current draft mode state.
    

Here's an example of how your layout server load function might conditionally include drafts:

src/routes/+layout.server.ts

```typescript
import { isDraftModeEnabled } from '$lib/draftMode.server';

export const load = async (event) => {
  const draftModeEnabled = isDraftModeEnabled(event);

  const data = await executeQuery(query, {
    includeDrafts: draftModeEnabled,
  });

  return { data, draftModeEnabled };
};
```

For a complete implementation including secure cookie handling with JWT tokens and API route handlers, check out the [draft mode implementation in the SvelteKit Starter Kit](https://github.com/datocms/sveltekit-starter-kit/tree/main/src/lib/draftMode.server.ts).

---

# SvelteKit — Managing images

Source [docs]: https://www.datocms.com/docs/svelte/managing-images.md

One of the major advantages of using DatoCMS instead of any other content management systems is its [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images), which will return pre-computed image attributes that will help you setting up responsive images in your frontend without any additional manipulation.

To make it even easier to offer responsive, progressive, lazy-loaded images on your projects, we offer a package called [`@datocms/svelte`](https://github.com/datocms/datocms-svelte/) that exposes an `<Image />` component and pairs perfectly with the `responsiveImage` query:

(Video content)

To take advantage of it, install the [`@datocms/svelte`](https://github.com/datocms/datocms-svelte) package:

Terminal window

```bash
yarn add @datocms/svelte
```

Before using the image component, it is necessary to obtain the necessary data by running a GraphQL query using [`responsiveImage`](/docs/content-delivery-api/images-and-videos.md#responsive-images):

src/routes/+page.server.ts

```javascript
const query = `
  query HomeQuery {
    blogPost {
    title
    cover {
      responsiveImage(imgixParams: { fit: crop, w: 300, h: 300, auto: format }) {
        # always required
        src
        srcSet
        width
        height

        # not required, but strongly suggested!
        alt
        title

        # LQIP (base64-encoded)
        base64

        # you can omit 'sizes' if you explicitly pass the 'sizes' prop to the image component
        sizes
      }
    }
  }
`;

export const load = () => {
  return executeQuery(query);
};
```

Then, inside your component or SvelteKit page, feed content coming from a `responsiveImage` query directly into the `<Image />` component:

src/routes/+page.svelte

```html
<script>
import { Image } from '@datocms/svelte';

export let data;
</script>

<Image data={data.blogPost.cover.responsiveImage} />
```

The [`@datocms/svelte`](https://github.com/datocms/datocms-svelte) package also offer a `<NakedImage />` component which generates minimum JS footprint, outputs a single `<picture />` element and implements lazy-loading using the native [`loading="lazy"` attribute](https://web.dev/articles/browser-level-image-lazy-loading). You can refer to the package [README](https://github.com/datocms/datocms-svelte/tree/main/src/lib/components/Image) to learn more.

---

# SvelteKit — Displaying videos

Source [docs]: https://www.datocms.com/docs/svelte/displaying-videos.md

> [!PROTIP] Pro tip: Start with our how-to guides first
> If you're new to hosting videos on DatoCMS, we recommend first starting with our tutorials:
> 
> -   How to upload videos: [Videos and Video Optimizations](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)
>     
> -   Why you should use HLS Streaming via Mux: [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md)
>     
> 
> Then, this page provides framework-specific playback advice using our helper components. Read on when you're ready!

One of the advantages of using DatoCMS instead of other content management systems is its `video` query, which will return **pre-computed video attributes that will help you display videos in your frontend without any additional manipulation**.

To make it easy to offer optimized, progressive videos on your projects, we offer a package called [`@datocms/svelte`](https://github.com/datocms/datocms-svelte/) that exposes a `<VideoPlayer />` component and pairs perfectly with the video query.

To take advantage of it, install the following packages:

Terminal window

```bash
yarn add @datocms/svelte @mux/mux-player
```

Before using the video player component, it is necessary to obtain the necessary data by running a GraphQL query:

src/routes/+page.server.ts

```javascript
const query = `
  query HomeQuery {
    blogPost {
    title
    coverVideo {
      video {
        # required: this field identifies the video to be played
        muxPlaybackId

        # all the other fields are not required but:

        # if provided, title is displayed in the upper left corner of the video
        title

        # if provided, width and height are used to define the aspect ratio of the
        # player, so to avoid layout jumps during the rendering.
        width
        height

        # if provided, it shows a blurred placeholder for the video
        blurUpThumb
      }
    }
  }
`;

export const load = () => {
  return executeQuery(query);
};
```

Then, inside your page, feed content coming from a `video` query directly into the `<VideoPlayer />` component:

src/routes/+page.svelte

```jsx
<script>
import { VideoPlayer } from '@datocms/svelte';

export let data;
</script>

<VideoPlayer data={data.blogPost.coverVideo.video} />
```

---

# SvelteKit — Structured Text fields

Source [docs]: https://www.datocms.com/docs/svelte/structured-text-fields.md

Rich text in DatoCMS is stored in [Structured Text](/docs/content-modelling/structured-text.md) fields, which lets us use it in many different contexts, from HTML in the browser to speech fulfillments in voice interfaces, if that's what you want.

There's a lot to be said about Structured Text and the extensibility of it, but for now let's just say that it returns content in a particular [JSON format called `dast`](/docs/structured-text/dast.md) which will resemble this example:

```json
{
  "schema": "dast",
  "document": {
    "type": "root",
    "children": [
      {
        "type": "heading",
        "level": 1,
        "children": [
          {
            "type": "span",
            "marks": [],
            "value": "Hello world!"
          }
        ]
      }
    ]
  }
}
```

To make it easy to convert this format in HTML inside your Svelte projects, we released a package called [`@datocms/svelte`](https://github.com/datocms/datocms-svelte/) that exposes a `<StructuredText />` component that does all the tedious work for you.

To take advantage of it, install the [`@datocms/svelte`](https://github.com/datocms/datocms-svelte/) package if you haven't already:

Terminal window

```bash
yarn add @datocms/svelte
```

Now let's make a [GraphQL query to fetch a Structured Text field:](/docs/content-delivery-api/structured-text-fields.md)

src/routes/+page.server.ts

```javascript
const query = `
  query HomeQuery {
    blogPost {
      title
      content {
        value
      }
    }
  }
`;

export const load = () => {
  return executeQuery(query);
};
```

We can now feed the result to the `data` prop of a `<StructuredText />` component:

src/routes/+page.svelte

```html
<script>
import { StructuredText } from '@datocms/svelte';

export let data;
</script>

<article>
  <h1>{{ data.blogPost.title }}</h1>
  <StructuredText data={data.blogPost.content} />
</article>
```

## Rendering special nodes

Other than "regular" formatting nodes (paragraphs, lists, etc.), Structured Text documents can contain four special types of node:

-   [`itemLink` nodes](/docs/structured-text/dast.md#itemLink) are just like regular HTML hyperlinks, but point to other records instead of URLs;
-   [`inlineItem` nodes](/docs/structured-text/dast.md#inlineItem) lets you directly embed a reference to a record in-between regular text;
    
-   [`block` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular paragraphs;
-   [`inlineBlock` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular text;
    

If a Structured Text document contains one of these nodes, then we need to change the GraphQL query, so that we also fetch all the records and blocks it references. As an example, if the field can link to other Blog posts, and can embed blocks of type "Image block" and "Mention block", then the query should change like this:

```javascript
const query = `query HomeQuery {
  blogPostfirst {
    id
    title
    content {
      value
      blocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on ImageBlockRecord {
          image { url alt }
        }
      }
      inlineBlocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on MentionBlockRecord {
          username
        }
      }
      links {
        ... on RecordInterface {
          id
          __typename
        }
        ... on BlogPostRecord {
          slug
          title
        }
      }
    }
  }
}`;
```

You must also tell `<StructuredText />` how to render such nodes. By using the `components` prop, you can declare an array of tuples composed of a predicate (a *predicate* is a function that takes one item as input and returns either true or false based on whether the item satisfies some condition) and a component: the predicate receives a node, and when it returns true, the custom component declared will be used to render the node:

```jsx
<script>
import { isBlock, isInlineItem, isItemLink } from 'datocms-structured-text-utils';

import { StructuredText } from '@datocms/svelte';

import Block from './Block.svelte';
import InlineBlock from './InlineBlock.svelte';
import InlineItem from './InlineItem.svelte';
import ItemLink from './ItemLink.svelte';
</script>

<StructuredText
  data={blogPost.content}
  components={[
    [isInlineItem, InlineItem],
    [isItemLink, ItemLink],
    [isBlock, Block],
    [isInlineBlock, InlineBlock],
  ]}
/>
```

---

# SvelteKit — SEO Management

Source [docs]: https://www.datocms.com/docs/svelte/seo-management.md

Similarly to what we offer with responsive images, our GraphQL API also offers a way to fetch [pre-computed SEO meta tags](/docs/content-delivery-api/seo-and-favicon.md) based on the content you insert inside DatoCMS.

You can easily use this information inside your Svelte app with the help of our [`@datocms/svelte`](https://github.com/datocms/datocms-svelte/) package.

Here's a sample of the meta tags you can automatically generate:

```html
<title>DatoCMS Blog - DatoCMS</title>
<meta property="og:title" content="DatoCMS Blog" />
<meta name="twitter:title" content="DatoCMS Blog" />
<meta name="description" content="Lorem ipsum..." />
<meta property="og:description" content="Lorem ipsum..." />
<meta name="twitter:description" content="Lorem ipsum..." />
<meta property="og:image" content="https://www.datocms-assets.com/..." />
<meta property="og:image:width" content="2482" />
<meta property="og:image:height" content="1572" />
<meta name="twitter:image" content="https://www.datocms-assets.com/..." />
<meta property="og:locale" content="en" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DatoCMS" />
<meta property="article:modified_time" content="2020-03-06T15:07:14Z" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@datocms" />
<link sizes="16x16" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="32x32" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="96x96" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="192x192" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
```

To do that, first install the [`@datocms/svelte`](https://github.com/datocms/datocms-svelte/) package.

Terminal window

```bash
yarn add @datocms/svelte
```

Then, inside your `+page.server.ts`, feed content coming from a `faviconMetaTags` or `_seoMetaTags` query:

src/routes/+page.server.ts

```javascript
const query = `
  query HomeQuery {
    site: _site {
      favicon: faviconMetaTags {
        attributes
        content
        tag
      }
    }
    blog {
      seo: _seoMetaTags {
        attributes
        content
        tag
      }
    }
  }
`;

export const load = () => {
  return executeQuery(query);
};
```

Then use the `<Head />` component to apply them to the page:

src/routes/+page.svelte

```html
<script>
  import { Head } from '@datocms/svelte';

  export let data;
</script>

<Head data={[...data.page.seo, ...data.site.favicon]} />
```

Want to know more about SEO customization in DatoCMS? Check out this video tutorial:

[

(Image content)

Working with and customizing SEO Fields

Play video »

](https://youtu.be/WjF10isSjS0)

---

# SvelteKit — Real-time updates

Source [docs]: https://www.datocms.com/docs/svelte/real-time-updates.md

Live updates can be extremely useful both for content editors and the regular visitors of your app/website:

-   Content-editors in Preview Mode can **see drafts directly in the production website**, without having to refresh the page;
-   Visitors can **immediately see new content as it gets published**, allowing all kinds of real-time interactions with your website/app (ie. live-news coverage).
    

(Video content)

Thanks to the [`querySubscription`](https://github.com/datocms/datocms-svelte/tree/main/src/lib/stores/querySubscription) store provided by the [@datocms/svelte](https://github.com/datocms/datocms-svelte) package you can get real-time updates for the page when the content changes. This function connects to the DatoCMS's [Real-time Updates API](/docs/real-time-updates-api/api-reference.md) to receive the updated query results in real-time, and is able to reconnect in case of network failures.

Live updates are great both to get instant previews of your content while editing it inside DatoCMS, or to offer real-time updates of content to your visitors (ie. news site).

### Reference

Please consult the [@datocms/svelte documentation](https://github.com/datocms/datocms-svelte/tree/main/src/lib/stores/querySubscription) to learn more about how to configure [`querySubscription`](https://github.com/datocms/datocms-svelte/tree/main/src/lib/stores/querySubscription), or take a look at the code of our Tech Starter Kit:

[

(Image content)

SvelteKit Starter Kit

Try this demo »

](https://www.datocms.com/marketplace/starters/sveltekit-starter-kit.md)

---

# SvelteKit — Visual Editing

Source [docs]: https://www.datocms.com/docs/svelte/visual-editing.md

Visual Editing represents the ultimate content management experience — the "holy grail" for content editors. Instead of navigating through forms and fields in a CMS interface, editors can see their content exactly as it appears on the live site, click directly on any element to edit it, and watch changes appear instantly.

This seamless experience is achieved by combining several techniques that work together:

1.  [**Draft Mode**](/docs/svelte/accessing-draft-updated-content-with-fetch.md) — Access unpublished content during preview sessions
    
2.  [**Real-time Updates**](/docs/svelte/real-time-updates.md) — See content changes reflected immediately without page refresh
    
3.  **Content Link** — Click-to-edit overlays that connect frontend elements to their CMS fields
    
4.  [**Web Previews Plugin**](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) — The DatoCMS plugin that orchestrates the editing experience
    

This guide focuses on **Content Link** and **Web Previews** — the final pieces that transform a preview into a true visual editing environment.

## Two levels of integration

Visual Editing can be set up incrementally:

##### Level 1: Content Link (standalone)

With just Content Link configured, editors browsing your website in draft mode can click on any content element to edit it. **Clicking opens DatoCMS in a new browser tab**, navigating directly to the field that controls that content.

This works entirely on your website — no DatoCMS plugin required. It's a great starting point that already provides significant value to editors.

(Video content)

Click-to-edit overlays

##### Level 2: Web Previews Plugin (side-by-side)

Adding the Web Previews plugin takes it further: editors can now **view the website and DatoCMS interface side-by-side within DatoCMS itself**. When they click on content, the edit panel opens instantly in the same view — no tab switching required.

The plugin also enables:

-   Preview links in the DatoCMS sidebar
-   Bidirectional navigation (browse the preview, and DatoCMS follows along)
    
-   Full-screen Visual Editing mode
    

(Video content)

Side-by-side editing

## Content Link: Click-to-edit overlays

Content Link enables the "click-to-edit" functionality by embedding invisible metadata (called "stega encoding") into your content. When editors hover over content in draft mode, visual overlays appear indicating which elements are editable.

**This works entirely on your website** — editors simply browse the site in draft mode, and clicking any editable element opens DatoCMS in a new tab. No plugin installation required.

(Image content)

Content Link overlays

##### How it works

1.  **Stega encoding** — When fetching draft content, pass the `contentLink` and `baseEditingUrl` options to embed invisible metadata into text fields
    
2.  **Detection** — The `<ContentLink />` component scans your page for this encoded content
    
3.  **Overlays** — Interactive overlays appear when editors hover over editable content
    
4.  **Deep linking** — Clicking an element opens DatoCMS at the exact field that controls that content
    

##### Setting up Content Link

The setup involves two parts:

**Enable stega encoding** when fetching draft content by passing the `contentLink` and `baseEditingUrl` options (see [`src/lib/datocms/queries.ts`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/lib/datocms/queries.ts) for the full implementation):

```javascript
executeQuery(query, {
  includeDrafts: true,
  contentLink: 'v1',
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
```

**Add the** **`<ContentLink />`** **component** to your root layout, rendered only in draft mode (see [`src/lib/components/ContentLink`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/lib/components/ContentLink/index.svelte) and [`src/routes/+layout.svelte`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/routes/+layout.svelte) for implementation details):

```html
<script>
  import { ContentLink } from '@datocms/svelte';
  import { goto } from '$app/navigation';
  import { page } from '$app/stores';
</script>

{#if data.draftModeEnabled}
  <ContentLink
    onNavigateTo={(path) => goto(path)}
    currentPath={$page.url.pathname}
    enableClickToEdit={{ hoverOnly: true }}
  />
{/if}
```

The SvelteKit integration uses `goto` from `$app/navigation` for client-side navigation and `$page.url.pathname` to track the current path — this enables the Web Previews plugin to stay in sync with your preview.

For component props and keyboard shortcuts, see the [@datocms/svelte ContentLink documentation](https://github.com/datocms/datocms-svelte/blob/main/src/lib/components/ContentLink/README.md).

##### Working with Structured Text

Structured Text fields require two rules for Visual Editing to work correctly.

**Rule 1: Always wrap the Structured Text component in a group.** This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```svelte
<div data-datocms-content-link-group>
  <StructuredText data={content.structuredText} />
</div>
```

**Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary.** These elements have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Note that record links (item links) do **not** need a boundary — their content belongs to the surrounding structured text, so there's no URL collision.

In your custom components, wrap the root element with `data-datocms-content-link-boundary`:

Block.svelte

```svelte
<script>
  export let block;
</script>

<div data-datocms-content-link-boundary>
  <h2>{block.title}</h2>
  <p>{block.description}</p>
</div>
```

InlineBlock.svelte

```svelte
<script>
  export let block;
</script>

<em data-datocms-content-link-boundary>{block.username}</em>
```

InlineItem.svelte

```svelte
<script>
  export let link;
</script>

<a href="/team/{link.slug}" data-datocms-content-link-boundary>{link.title}</a>
```

Then pass them to `StructuredText`:

```svelte
<script>
  import { StructuredText } from '@datocms/svelte';
  import { isBlock, isInlineBlock, isInlineItem, isItemLink } from 'datocms-structured-text-utils';
  import Block from './Block.svelte';
  import InlineBlock from './InlineBlock.svelte';
  import InlineItem from './InlineItem.svelte';
  import ItemLink from './ItemLink.svelte';
</script>

<div data-datocms-content-link-group>
  <StructuredText
    data={page.content}
    components={[
      [isBlock, Block],
      [isInlineBlock, InlineBlock],
      [isInlineItem, InlineItem],
      [isItemLink, ItemLink],
    ]}
  />
</div>
```

See the [ContentLink Structured Text documentation](https://github.com/datocms/datocms-svelte/blob/main/src/lib/components/ContentLink/README.md#structured-text-fields) for details.

## Web Previews Plugin (optional enhancement)

The [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) plugin enhances the editing experience by embedding your website preview directly inside DatoCMS. Instead of switching between browser tabs, editors get a **side-by-side view** where clicking on content instantly opens the edit panel.

When Content Link detects it's running inside the Web Previews plugin iframe, it automatically switches from opening new tabs to communicating with the plugin — no code changes required.

(Image content)

Side-by-side editing in DatoCMS

##### How it works

The plugin communicates with your frontend through two API endpoints:

**Preview Links API** — Receives record info from DatoCMS and returns preview URLs (see [`src/routes/api/preview-links/+server.ts`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/routes/api/preview-links/%2Bserver.ts) for the full implementation):

src/routes/api/preview-links/+server.ts

```typescript
export const POST: RequestHandler = async ({ url, request }) => {
  const { item, itemType, locale } = await request.json();
  const recordUrl = recordToWebsiteRoute(item, itemType.id, locale);

  return json({
    previewLinks: [
      { label: 'Draft version', url: `/api/draft-mode/enable?redirect=${recordUrl}` }
    ]
  });
};
```

**Enable Draft Mode route** — Activates draft mode and redirects to the preview. This is the same route [covered in the Draft Mode guide](https://file+.vscode-resource.vscode-cdn.net/Users/stefanoverna/dato/tech-starters/sveltekit/guide/accessing-draft-updated-content-with-fetch).

##### Configuring the plugin

In your DatoCMS project:

1.  Install the [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) from the marketplace
    
2.  Configure a frontend with:
    
    -   **Preview Links API endpoint**: `https://yoursite.com/api/preview-links?token=your-secret`
        
    -   **Enable Draft Mode route**: `https://yoursite.com/api/draft-mode/enable?token=your-secret`
        

For full configuration details, see the [Web Previews plugin documentation](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md#installation-and-configuration).

> [!WARNING] Content Security Policy
> If your website implements a Content Security Policy with a `frame-ancestors` directive, you need to allow the DatoCMS plugin to embed your site. In SvelteKit, this is typically configured via the `handle` hook in `hooks.server.ts`:
> 
> src/hooks.server.ts
> 
> ```typescript
> export const handle: Handle = async ({ event, resolve }) => {
>   const response = await resolve(event);
> 
> 
>   response.headers.set(
>     'Content-Security-Policy',
>     "frame-ancestors 'self' https://plugins-cdn.datocms.com"
>   );
> 
> 
>   return response;
> };
> ```

---

# Astro — Astro + DatoCMS Overview

Source [docs]: https://www.datocms.com/docs/astro.md

Astro is a modern static site generator and web framework that allows developers to build fast, **content-focused websites** using multiple frontend frameworks simultaneously. Its key differentiator is its "partial hydration" approach, which only sends JavaScript to the browser when necessary, resulting in extremely lightweight and fast-loading pages - making it particularly well-suited for content-heavy sites like blogs, documentation, and marketing pages where performance is crucial.

DatoCMS is the perfect companion to Astro.js since it offers content, images and videos on a globally-distributed CDN. With this combo, you can have an **infinitely scalable website, ready to handle prime-time TV traffic spikes at a fraction of the regular cost.**

In the next paragraphs, will see how easy it is to combine Astro with DatoCMS.

### Fetching content from our GraphQL API

Let's start by installing `@datocms/cda-client`, a lightweight, TypeScript-ready package that offers various helpers around the native Fetch API to perform GraphQL requests towards [DatoCMS Content Delivery API](/docs/content-delivery-api/api-endpoints.md):

Terminal window

```bash
npm install --save @datocms/cda-client
```

We can now create a function we can use in all of our pages and components that need to fetch content from DatoCMS.

Create a new directory called `lib`, and inside of it, add a file called `datocms.js`:

```javascript
import { executeQuery as libExecuteQuery } from "@datocms/cda-client";
import { DATOCMS_CDA_TOKEN } from "astro:env/server";

export async function executeQuery(query, options) {
  return await libExecuteQuery(query, {
    ...options,
    token: DATOCMS_CDA_TOKEN,
  });
}
```

Make sure you set `DATOCMS_CDA_TOKEN` as an actual API token of your DatoCMS project. You can create a new one under "Settings \> API Tokens".

(Video content)

We can now effortlessly build our first Astro page, using data from DatoCMS:

src/pages/index.astro

```javascript
---
const query = `
  query HomeQuery {
    blogPost { title }
  }
`;

const data = await executeQuery(query);
---

<article>
  <h1>{data.blogPost.title}</h1>
</article>
```

You can learn everything you need regarding how to build GraphQL queries on our [Content Delivery API documentation](/docs/content-delivery-api.md).

---

# Astro — Accessing draft/updated content

Source [docs]: https://www.datocms.com/docs/astro/accessing-draft-updated-content.md

If you have [draft/published mode](/docs/general-concepts/draft-published.md) enabled on some of your models, you can use [the `X-Include-Drafts` header](/docs/content-delivery-api/api-endpoints.md#include-drafts) to **access records at their latest version available** instead of the currently published one

Pages and layouts can utilize the `includeDrafts` option of the `executeQuery` function in their server load functions:

src/pages/index.astro

```javascript
---
const query = `
  query HomeQuery {
    blogPost { title }
  }
`;

const data = await executeQuery(query, { includeDrafts: true });
---

<article>
  <h1>{data.blogPost.title}</h1>
</article>
```

The `X-Include-Drafts` is one of many headers you can use to shape up the behavior of the Content Delivery API. Check out the other [available headers in the Content Delivery API](/docs/content-delivery-api/api-endpoints.md).

### Setting up Draft Mode

Hardcoding `includeDrafts: true` is useful during development, but what if you want to toggle between draft and published content on a deployed website? This is where "Draft Mode" comes in.

Unlike Next.js, Astro doesn't provide a built-in draft mode feature, but you can implement one yourself using **cookies** to persist the draft mode state across requests. This approach works when your Astro site is configured for **server-side rendering** (SSR) or hybrid mode.

The basic idea involves:

-   Creating API routes to enable/disable draft mode by setting/removing a secure cookie
-   Creating a helper function to check whether draft mode is currently active
    
-   Passing the draft mode status to `executeQuery` calls throughout your pages
    

Here's a simplified example of how your pages would use draft mode:

src/pages/index.astro

```javascript
---
import { isDraftModeEnabled } from '~/lib/draftMode';

const draftMode = isDraftModeEnabled(Astro.cookies);

const query = `
  query HomeQuery {
    blogPost { title }
  }
`;

const data = await executeQuery(query, { includeDrafts: draftMode });
---

<article>
  <h1>{data.blogPost.title}</h1>
</article>
```

Our [Astro starter kit](https://github.com/datocms/astro-starter-kit) includes a complete implementation of this pattern. You can see the [draft mode helpers](https://github.com/datocms/astro-starter-kit/blob/main/src/lib/draftMode.ts) that handle secure cookie management with JWT tokens, and the [API routes](https://github.com/datocms/astro-starter-kit/tree/main/src/pages/api/draft-mode) that enable and disable draft mode.

---

# Astro — Managing images

Source [docs]: https://www.datocms.com/docs/astro/managing-images.md

One of the major advantages of using DatoCMS instead of any other content management systems is its [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images), which will return pre-computed image attributes that will help you setting up responsive images in your frontend without any additional manipulation.

To make it even easier to offer responsive, progressive, lazy-loaded images on your projects, we offer a package called [`@datocms/astro`](https://github.com/datocms/astro-datocms) that exposes an `<Image />` component and pairs perfectly with the `responsiveImage` query:

(Video content)

To take advantage of it, install the [`@datocms/astro`](https://github.com/datocms/astro-datocms) package:

Terminal window

```bash
yarn add @datocms/astro
```

Before using the image component, it is necessary to obtain the necessary data by running a GraphQL query using [`responsiveImage`](/docs/content-delivery-api/images-and-videos.md#responsive-images).

Then, inside your Astro component or page, feed content coming from a `responsiveImage` query directly into the `<Image />` component:

src/pages/index.astro

```javascript
---
import { Image } from '@datocms/astro';

const query = `
  query HomeQuery {
    blogPost {
    title
    cover {
      responsiveImage(imgixParams: { fit: crop, w: 300, h: 300, auto: format }) {
        # always required
        src
        srcSet
        width
        height

        # not required, but strongly suggested!
        alt
        title

        # LQIP (base64-encoded)
        base64

        # you can omit 'sizes' if you explicitly pass the 'sizes' prop to the image component
        sizes
      }
    }
  }
`;
const data = await executeQuery(query);
---

<article>
  <h1>{data.blogPost.title}</h1>
  <Image data={data.blogPost.cover.responsiveImage} />
</article>
```

The image component creates zero JS footprint, produces a single <picture /\> element, and implements lazy-loading using the native [`loading="lazy"` attribute](https://web.dev/articles/browser-level-image-lazy-loading). You can refer to the package [README](https://github.com/datocms/astro-datocms/tree/main/src/Image) to learn more.

---

# Astro — Displaying videos

Source [docs]: https://www.datocms.com/docs/astro/displaying-videos.md

> [!PROTIP] Pro tip: Start with our how-to guides first
> If you're new to hosting videos on DatoCMS, we recommend first starting with our tutorials:
> 
> -   How to upload videos: [Videos and Video Optimizations](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)
>     
> -   Why you should use HLS Streaming via Mux: [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md)
>     
> 
> Then, this page provides framework-specific playback advice using our helper components. Read on when you're ready!

One of the advantages of using DatoCMS instead of other content management systems is its [`video`](/docs/content-delivery-api/images-and-videos.md#videos) query, which will return **pre-computed video attributes that will help you display videos in your frontend without any additional manipulation**.

Let's begin by defining our GraphQL query, which is necessary to retrieve data for the video player:

src/pages/index.astro

```javascript
---
const query = `
  query HomeQuery {
    blogPost {
    title
    coverVideo {
      video {
        # required: this field identifies the video to be played
        muxPlaybackId

        # all the other fields are not required but:

        # if provided, title is displayed in the upper left corner of the video
        title

        # if provided, width and height are used to define the aspect ratio of the
        # player, so to avoid layout jumps during the rendering.
        width
        height

        # if provided, it shows a blurred placeholder for the video
        blurUpThumb
      }
    }
  }
`;
const data = await executeQuery(query);
---

<article>
  <h1>{data.blogPost.title}</h1>
  ...
```

The video player will be an [Astro Island](https://docs.astro.build/en/concepts/islands/), pre-rendered on the server, and then re-hydrated on the client using [client directives](https://docs.astro.build/en/reference/directives-reference/#client-directives).

The [UI framework](https://docs.astro.build/en/guides/framework-components/) for this island can be any among [React](https://github.com/datocms/react-datocms/blob/master/docs/video-player.md), [Vue](https://github.com/datocms/vue-datocms/tree/master/src/components/VideoPlayer), or [SvelteKit](https://github.com/datocms/datocms-svelte/tree/main/src/lib/components/VideoPlayer), since we have developed a `<VideoPlayer />` component for each of these choices. Choose based on your personal preferences.

For the purposes of this guide, we will choose React, and therefore we will install the [`react-datocms`](https://github.com/datocms/react-datocms) package:

```plaintext
npm install react-datocms
```

Now you can feed content coming from a `video` query directly into the `<VideoPlayer />` component:

src/pages/index.astro

```jsx
---
import { VideoPlayer } from '@datocms/react';

const query = `...`;

const data = await executeQuery(query);
---

<VideoPlayer data={data.blogPost.coverVideo.video} client:visible />
```

The `client:visible` prop is used to ensure that the component loads and hydrates once the component has entered the user’s viewport. However, you can choose any among the other [client directives](https://docs.astro.build/en/reference/directives-reference/#client-directives) made available by Astro.

---

# Astro — Structured Text fields

Source [docs]: https://www.datocms.com/docs/astro/structured-text-fields.md

Rich text in DatoCMS is stored in [Structured Text](/docs/content-modelling/structured-text.md) fields, which lets us use it in many different contexts, from HTML in the browser to speech fulfillments in voice interfaces, if that's what you want.

There's a lot to be said about Structured Text and the extensibility of it, but for now let's just say that it returns content in a particular [JSON format called `dast`](/docs/structured-text/dast.md) which will resemble this example:

```json
{
  "schema": "dast",
  "document": {
    "type": "root",
    "children": [
      {
        "type": "heading",
        "level": 1,
        "children": [
          {
            "type": "span",
            "marks": [],
            "value": "Hello world!"
          }
        ]
      }
    ]
  }
}
```

To make it easy to convert this format in HTML inside your Astro projects, we released a package called [`@datocms/astro`](https://github.com/datocms/astro-datocms) that exposes a `<StructuredText />` component that does all the tedious work for you.

To take advantage of it, install the [`@datocms/astro`](https://github.com/datocms/astro-datocms) package if you haven't already:

Terminal window

```bash
yarn add @datocms/astro
```

Now let's make a [GraphQL query to fetch a Structured Text field](/docs/content-delivery-api/structured-text-fields.md) and feed the result to the `data` prop of a `<StructuredText />` component:

src/pages/index.astro

```javascript
---
import { StructuredText } from '@datocms/astro';

const query = `
  query HomeQuery {
    blogPost {
      title
      content {
        value
      }
    }
  }
`;
const data = await executeQuery(query);
---

<StructuredText data={data.blogPost.content} />
```

## Rendering special nodes

Other than "regular" formatting nodes (paragraphs, lists, etc.), Structured Text documents can contain four special types of node:

-   [`itemLink` nodes](/docs/structured-text/dast.md#itemLink) are just like regular HTML hyperlinks, but point to other records instead of URLs;
-   [`inlineItem` nodes](/docs/structured-text/dast.md#inlineItem) lets you directly embed a reference to a record in-between regular text;
    
-   [`block` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular paragraphs;
-   [`inlineBlock` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular text;
    

If a Structured Text document contains one of these nodes, then we need to change the GraphQL query, so that we also fetch all the records and blocks it references. As an example, if the field can link to other Blog posts, and can embed blocks of type "Image block" and "Mention block", then the query should change like this:

```javascript
const query = `query HomeQuery {
  blogPostfirst {
    id
    title
    content {
      value
      blocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on ImageBlockRecord {
          image { url alt }
        }
      }
      inlineBlocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on MentionBlockRecord {
          username
        }
      }
      links {
        ... on RecordInterface {
          id
          __typename
        }
        ... on BlogPostRecord {
          slug
          title
        }
      }
    }
  }
}`;
```

You also need to instruct `<StructuredText />` on how to display these nodes. This can be done by using the `blockComponents`, `inlineRecordComponents`, and `linkToRecordComponents` props to specify the Astro component to render the node.

src/pages/index.astro

```jsx
---
import { StructuredText } from '@datocms/astro';

import ImageBlock from '~/components/ImageBlock/index.astro';
import MentionBlock from '~/components/MentionBlock/index.astro';
import InlineBlogPost from '~/components/InlineBlogPost/index.astro';
import LinkToBlogPost from '~/components/LinkToBlogPost/index.astro';
---

<StructuredText
  data={blogPost.content}
  blockComponents={{
    ImageBlockRecord: ImageBlock,
  }}
  inlineBlockComponents={{
    MentionBlockRecord: MentionBlock,
  }}
  inlineRecordComponents={{
    BlogPostRecord: InlineBlogPost,
  }}
  linkToRecordComponents={{
    BlogPostRecord: LinkToBlogPost,
  }}
/>
```

The following rules will apply:

-   Astro components passed in `blockComponents` and `inlineBlockComponents` will be used to render blocks and will receive a `block` prop containing the actual block data.
-   Astro components passed in `inlineRecordComponents` will be used to render inline records and will receive a `record` prop containing the actual record.
    
-   Astro components passed in `linkToRecordComponents` will be used to render links to records and will receive the following props: `node` (the actual `'inlineItem'` node), `record` (the record linked to the node), and `attrs` (the custom attributes for the link specified by the node).

---

# Astro — SEO Management

Source [docs]: https://www.datocms.com/docs/astro/seo-management.md

Similarly to what we offer with responsive images, our GraphQL API also offers a way to fetch [pre-computed SEO meta tags](/docs/content-delivery-api/seo-and-favicon.md) based on the content you insert inside DatoCMS.

You can easily use this information inside your Svelte app with the help of our [`@datocms/astro`](https://github.com/datocms/astro-datocms/) package.

Here's a sample of the meta tags you can automatically generate:

```html
<title>DatoCMS Blog - DatoCMS</title>
<meta property="og:title" content="DatoCMS Blog" />
<meta name="twitter:title" content="DatoCMS Blog" />
<meta name="description" content="Lorem ipsum..." />
<meta property="og:description" content="Lorem ipsum..." />
<meta name="twitter:description" content="Lorem ipsum..." />
<meta property="og:image" content="https://www.datocms-assets.com/..." />
<meta property="og:image:width" content="2482" />
<meta property="og:image:height" content="1572" />
<meta name="twitter:image" content="https://www.datocms-assets.com/..." />
<meta property="og:locale" content="en" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DatoCMS" />
<meta property="article:modified_time" content="2020-03-06T15:07:14Z" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@datocms" />
<link sizes="16x16" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="32x32" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="96x96" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="192x192" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
```

To do that, first install the [`@datocms/astro`](https://github.com/datocms/astro-datocms) package:

Terminal window

```bash
yarn add @datocms/astro
```

Then, inside your page, feed content coming from a `faviconMetaTags` or `_seoMetaTags` query, then use the `<Seo />` component to apply them inside the page `<head>`:

src/pages/index.astro

```javascript
---
const query = `
  query HomeQuery {
    site: _site {
      favicon: faviconMetaTags {
        attributes
        content
        tag
      }
    }
    blog {
      seo: _seoMetaTags {
        attributes
        content
        tag
      }
    }
  }
`;

const data = await executeQuery(query);
---

<!doctype html>
<html lang="en">
  <head>
    <Seo data={[...data.data.page.seo, ...data.data.site.favicon]} />
  </head>
  ...
```

Want to know more about SEO customization in DatoCMS? Check out this video tutorial:

[

(Image content)

Working with and customizing SEO Fields

Play video »

](https://youtu.be/WjF10isSjS0)

---

# Astro — Real-time updates

Source [docs]: https://www.datocms.com/docs/astro/real-time-updates.md

Live updates can be extremely useful both for content editors and the regular visitors of your app/website:

-   Content-editors in Draft Mode can **see drafts directly in the production website**, without having to refresh the page;
-   Visitors can **immediately see new content as it gets published**, allowing all kinds of real-time interactions with your website/app (e.g., live-news coverage).
    

The [`<QueryListener />`](https://github.com/datocms/astro-datocms/tree/main/src/QueryListener) component from the `@datocms/astro` package provides real-time page reload when content changes. It connects to DatoCMS's [Real-time Updates API](/docs/real-time-updates-api/api-reference.md) to receive updated query results in real-time, and is able to reconnect in case of network failures.

Simply add the component at the end of your page, passing the same query and variables you used to fetch content:

src/pages/index.astro

```javascript
---
import { QueryListener } from '@datocms/astro';
import { DATOCMS_CDA_TOKEN } from "astro:env/server";

const query = `
  query HomeQuery {
    blogPost { title }
  }
`;

const data = await executeQuery(query, { includeDrafts: true });
---

<article>
  <h1>{data.blogPost.title}</h1>
</article>

<QueryListener
  token={DATOCMS_CDA_TOKEN}
  includeDrafts
  query={query}
/>
```

### Draft Mode + `<QueryListener />`

Perhaps a more common scenario is activating real-time updates **only for content editors in Draft Mode**. To avoid repetitive code, you can create a simple wrapper component that checks draft mode status before rendering:

src/components/DraftModeQueryListener.astro

```javascript
---
import { QueryListener } from '@datocms/astro';
import { DATOCMS_CDA_TOKEN } from 'astro:env/server';
import { isDraftModeEnabled } from '~/lib/draftMode';

const draftModeEnabled = isDraftModeEnabled(Astro.cookies);
---

{
  draftModeEnabled && (
    <QueryListener
      {...Astro.props}
      token={DATOCMS_CDA_TOKEN}
      includeDrafts
    />
  )
}
```

Then use it in your pages to enable real-time updates for editors only:

src/pages/\[slug\].astro

```javascript
---
import { DraftModeQueryListener } from '~/components/DraftModeQueryListener';

const query = `...`;
const data = await executeQuery(query, { includeDrafts: draftModeEnabled });
---

<article>...</article>

<DraftModeQueryListener query={query} variables={{ slug }} />
```

### Reference

Please consult the [@datocms/astro documentation](https://github.com/datocms/astro-datocms/tree/main/src/QueryListener) to learn more about how to configure [`<QueryListener />`](https://github.com/datocms/astro-datocms/tree/main/src/QueryListener). You can also look at a real-world example in the [Astro Starter Kit](https://github.com/datocms/astro-starter-kit), which includes a [`DraftModeQueryListener`](https://github.com/datocms/astro-starter-kit/blob/main/src/components/DraftModeQueryListener/Component.astro) wrapper component.

---

# Astro — Visual Editing

Source [docs]: https://www.datocms.com/docs/astro/visual-editing.md

Visual Editing represents the ultimate content management experience — the "holy grail" for content editors. Instead of navigating through forms and fields in a CMS interface, editors can see their content exactly as it appears on the live site, click directly on any element to edit it, and watch changes appear instantly.

This seamless experience is achieved by combining several techniques that work together:

1.  [**Draft Mode**](/docs/astro/accessing-draft-updated-content.md) — Access unpublished content during preview sessions
    
2.  [**Real-time Updates**](/docs/astro/real-time-updates.md) — See content changes reflected immediately without page refresh
    
3.  **Content Link** — Click-to-edit overlays that connect frontend elements to their CMS fields
    
4.  [**Web Previews Plugin**](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) — The DatoCMS plugin that orchestrates the editing experience
    

This guide focuses on **Content Link** and **Web Previews** — the final pieces that transform a preview into a true visual editing environment.

## Two levels of integration

Visual Editing can be set up incrementally:

##### Level 1: Content Link (standalone)

With just Content Link configured, editors browsing your website in draft mode can click on any content element to edit it. **Clicking opens DatoCMS in a new browser tab**, navigating directly to the field that controls that content.

This works entirely on your website — no DatoCMS plugin required. It's a great starting point that already provides significant value to editors.

(Video content)

Click-to-edit overlays

##### Level 2: Web Previews Plugin (side-by-side)

Adding the Web Previews plugin takes it further: editors can now **view the website and DatoCMS interface side-by-side within DatoCMS itself**. When they click on content, the edit panel opens instantly in the same view — no tab switching required.

The plugin also enables:

-   Preview links in the DatoCMS sidebar
-   Bidirectional navigation (browse the preview, and DatoCMS follows along)
    
-   Full-screen Visual Editing mode
    

(Video content)

Side-by-side editing

## Content Link: Click-to-edit overlays

Content Link enables the "click-to-edit" functionality by embedding invisible metadata (called "stega encoding") into your content. When editors hover over content in draft mode, visual overlays appear indicating which elements are editable.

**This works entirely on your website** — editors simply browse the site in draft mode, and clicking any editable element opens DatoCMS in a new tab. No plugin installation required.

(Image content)

Content Link overlays

##### How it works

1.  **Stega encoding** — When fetching draft content, pass the `contentLink` and `baseEditingUrl` options to embed invisible metadata into text fields
    
2.  **Detection** — The `<ContentLink />` component scans your page for this encoded content
    
3.  **Overlays** — Interactive overlays appear when editors hover over editable content
    
4.  **Deep linking** — Clicking an element opens DatoCMS at the exact field that controls that content
    

##### Setting up Content Link

The setup involves two parts:

1.  **Enable stega encoding** when fetching draft content by passing the `contentLink` and `baseEditingUrl` options:
    

```javascript
executeQuery(query, {
  includeDrafts: true,
  contentLink: 'v1',
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
```

See [`src/lib/datocms/executeQuery.ts`](https://github.com/datocms/astro-starter-kit/blob/main/src/lib/datocms/executeQuery.ts) for the full implementation.

1.  **Add the** **`<ContentLink />`** **component** to your root layout, rendered only in draft mode:
    

```jsx
---
import { ContentLink } from '@datocms/astro/ContentLink';
import { isDraftModeEnabled } from '~/lib/draftMode';

const draftModeEnabled = isDraftModeEnabled(Astro.cookies);
---

<html>
  <body>
    {draftModeEnabled && <ContentLink enableClickToEdit={{ hoverOnly: true }} />}
    <slot />
  </body>
</html>
```

See [`src/layouts/Layout.astro`](https://github.com/datocms/astro-starter-kit/blob/main/src/layouts/Layout.astro) for the full implementation.

The `hoverOnly` option ensures click-to-edit is only enabled on devices with hover capability (non-touch), avoiding interference with touch scrolling. On touch devices, editors can still toggle click-to-edit by pressing the Alt/Option key.

For component props and keyboard shortcuts, see the [astro-datocms ContentLink documentation](https://github.com/datocms/astro-datocms/blob/main/src/ContentLink/README.md#props).

##### Working with Structured Text

Structured Text fields require two rules for Visual Editing to work correctly.

**Rule 1: Always wrap the Structured Text component in a group.** This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```astro
<div data-datocms-content-link-group>
  <StructuredText data={content.body} />
</div>
```

**Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary.** These elements have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Note that record links do **not** need a boundary — they are just `<a>` tags wrapping text that belongs to the surrounding structured text, so there's no URL collision.

Add `data-datocms-content-link-boundary` to the root element of each component that renders a block, inline block, or inline record:

src/components/Cta.astro

```astro
---
const { block } = Astro.props;
---

<a href={block.url} data-datocms-content-link-boundary>
  {block.label}
</a>
```

The same applies to inline records and inline blocks:

src/components/InlineTeamMember.astro

```astro
---
const { record } = Astro.props;
---

<a href={`/team/${record.slug}`} data-datocms-content-link-boundary>
  {record.name}
</a>
```

Then use these components in your structured text rendering:

```astro
---
import { StructuredText } from '@datocms/astro/StructuredText';
import Cta from '~/components/Cta.astro';
import InlineTeamMember from '~/components/InlineTeamMember.astro';
---

<div data-datocms-content-link-group>
  <StructuredText
    data={page.content}
    blockComponents={{
      CtaRecord: Cta,
    }}
    inlineBlockComponents={{
      NewsletterSignupRecord: NewsletterSignup,
    }}
    inlineRecordComponents={{
      TeamMemberRecord: InlineTeamMember,
    }}
  />
</div>
```

See the [ContentLink Structured Text documentation](https://github.com/datocms/astro-datocms/blob/main/src/ContentLink/README.md#structured-text-fields) for more details.

## Web Previews Plugin (optional enhancement)

The [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) plugin enhances the editing experience by embedding your website preview directly inside DatoCMS. Instead of switching between browser tabs, editors get a **side-by-side view** where clicking on content instantly opens the edit panel.

When Content Link detects it's running inside the Web Previews plugin iframe, it automatically switches from opening new tabs to communicating with the plugin — no code changes required.

(Image content)

Side-by-side editing in DatoCMS

##### How it works

The plugin communicates with your frontend through two API endpoints:

**Preview Links API** — Receives record info from DatoCMS and returns preview URLs (see [`src/pages/api/preview-links/index.ts`](https://github.com/datocms/astro-starter-kit/blob/main/src/pages/api/preview-links/index.ts) for the full implementation):

src/pages/api/preview-links/index.ts

```typescript
import type { APIRoute } from 'astro';
import { recordToWebsiteRoute } from '~/lib/datocms/recordInfo';

export const POST: APIRoute = async ({ url, request }) => {
  const token = url.searchParams.get('token');

  // Validate the request token
  if (token !== SECRET_API_TOKEN) {
    return new Response('Invalid token', { status: 401 });
  }

  const { item, itemType, locale } = await request.json();
  const recordUrl = recordToWebsiteRoute(item, itemType.attributes.api_key, locale);

  const previewLinks = [];

  if (recordUrl && item.meta.status !== 'published') {
    previewLinks.push({
      label: 'Draft version',
      url: `/api/draft-mode/enable?redirect=${recordUrl}&token=${token}`,
    });
  }

  return new Response(JSON.stringify({ previewLinks }));
};
```

**Enable Draft Mode route** — Activates draft mode and redirects to the preview. This is the same route used for regular draft mode, covered in the [Draft Mode guide](/docs/astro/accessing-draft-updated-content.md). See [`src/pages/api/draft-mode/enable/index.ts`](https://github.com/datocms/astro-starter-kit/blob/main/src/pages/api/draft-mode/enable/index.ts) for the implementation.

##### Configuring the plugin

In your DatoCMS project:

1.  Install the [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) from the marketplace
    
2.  Configure a frontend with:
    
    -   **Preview Links API endpoint**: `https://yoursite.com/api/preview-links?token=your-secret`
        
    -   **Enable Draft Mode route**: `https://yoursite.com/api/draft-mode/enable?token=your-secret`
        

For full configuration details, see the [Web Previews plugin documentation](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md#installation-and-configuration).

> [!WARNING] Content Security Policy
> If your website implements a Content Security Policy with a `frame-ancestors` directive, you need to allow the DatoCMS plugin to embed your site. In Astro, this can be configured via server middleware or your hosting platform:
> 
> ```http
> Content-Security-Policy: frame-ancestors 'self' https://plugins-cdn.datocms.com;
> ```
> 
> See [Astro's security headers documentation](https://docs.astro.build/en/guides/middleware/) for implementation options.

---

# Astro — DatoCMS Cache Tags

Source [docs]: https://www.datocms.com/docs/astro/using-cache-tags.md

The DatoCMS Content Delivery API offers a feature called [cache tags](/docs/content-delivery-api/cache-tags.md). It lets you cache your website pages for as long as you like — serving traffic from a CDN instead of querying us on every request, which cuts your hosting bill and your DatoCMS API usage alike — without the usual headache of working out what to invalidate, and when.

Astro is a particularly good place to use it, because since Astro 7 tagged caching is a **first-class primitive of the framework** rather than something you bolt on. `Astro.cache.set({ tags })` records tags for the response being rendered, `cache.invalidate({ tags })` purges them, and your adapter's cache provider translates both into whatever your host actually speaks — `Cache-Tag` on Cloudflare, `Netlify-Cache-Tag` on Netlify.

So the integration is short: ask the Content Delivery API for the tags behind each query, hand them to `Astro.cache`, and hand the webhook's tags to `cache.invalidate()`. Roughly forty lines, most of which you write once in a shared `executeQuery` wrapper and then never think about again.

> [!PROTIP] Pro tip: The full story
> This guide is the Astro-specific version of a mechanism documented in full elsewhere. The [Cache Tags overview](/docs/content-delivery-api/cache-tags.md) explains it in three steps, [Cache tags in CDA responses](/docs/content-delivery-api/cache-tags-format.md) covers the header format and limits, and [the invalidation webhook](/docs/content-delivery-api/cache-tags-invalidation.md) covers the other direction.

### What you'll need

-   **Astro 7 or later**, with `output: 'server'`. Cache tags only make sense for server-rendered pages: a statically prerendered page has no response for the CDN to tag.
-   **An adapter that ships a cache provider** — [Cloudflare](https://docs.astro.build/en/guides/integrations-guide/cloudflare/), [Netlify](https://docs.astro.build/en/guides/integrations-guide/netlify/) and [Vercel](https://docs.astro.build/en/guides/integrations-guide/vercel/) all do. The examples below use Cloudflare; the last section covers what changes on the other two.
    
-   **A DatoCMS project on a plan that exposes cache tags**, and a read-only CDA token.
    

### Step 1: Enable the cache provider

Point Astro's `cache.provider` at your adapter's provider:

astro.config.mjs

```javascript
import cloudflare from '@astrojs/cloudflare';
import { cacheCloudflare } from '@astrojs/cloudflare/cache';
import { defineConfig } from 'astro/config';

export default defineConfig({
  output: 'server',
  adapter: cloudflare(),
  cache: {
    provider: cacheCloudflare(),
  },
});
```

On Cloudflare there's a second switch, and it's the one people miss: the Workers cache has to be turned on in `wrangler.jsonc` too (with Wrangler 4.69.0 or later). Without it, everything below runs without error and simply never caches anything.

wrangler.jsonc

```jsonc
{
  "name": "my-site",
  "main": "@astrojs/cloudflare/entrypoints/server",
  "compatibility_date": "2026-08-08",
  "cache": {
    "enabled": true,
  },
}
```

> [!WARNING] The cache is a no-op in `astro dev`
> Astro deliberately disables the cache provider in development mode, so `cache.set()` does nothing and no headers are emitted. To see cache tags at work you need a production build — `astro build && wrangler dev`, or a real deploy.

### Step 2: Ask DatoCMS for cache tags, and register them

Cache tags are returned in the `x-cache-tags` response header, but only if you ask for them. With [`@datocms/cda-client`](https://github.com/datocms/cda-client) that's the `returnCacheTags` option — and since you need the header and not just the parsed body, you want `rawExecuteQuery`, which returns both.

Everything then funnels into a single `executeQuery` wrapper that every page and component uses:

src/lib/datocms/executeQuery.js

```javascript
import { rawExecuteQuery } from '@datocms/cda-client';
import { DATOCMS_CDA_TOKEN } from 'astro:env/server';

const ONE_YEAR = 60 * 60 * 24 * 365;

export async function executeQuery(context, query, variables) {
  const [result, response] = await rawExecuteQuery(query, {
    token: DATOCMS_CDA_TOKEN,
    returnCacheTags: true,
    variables,
  });

  const rawTags = response.headers.get('x-cache-tags');
  const cacheTags = rawTags ? rawTags.split(' ') : [];

  if (cacheTags.length > 0) {
    context.cache.set({ maxAge: ONE_YEAR, tags: cacheTags });
  }

  return result;
}
```

Three details worth pausing on:

-   **The header is space-separated.** DatoCMS uses spaces (the Fastly `Surrogate-Key` convention); Cloudflare's `Cache-Tag` uses commas. You split on spaces, hand Astro an array, and the provider re-joins with commas for you.
-   **Tags accumulate across the whole request.** `Astro.cache` keeps a `Set` internally, so calling `cache.set()` once per query is exactly right: a page that runs a layout query, a header query and a page query ends up with the union of all three tag sets, deduplicated. You never have to collect tags yourself.
    
-   **`maxAge`** **is a year on purpose.** The cached response isn't meant to expire on a timer — it's meant to live until the content behind it changes, at which point step 4 purges it explicitly. A TTL here would only mean serving stale content for up to that TTL, and re-fetching content that hasn't changed. If you'd rather have a safety net, a `swr` window is a better tool than a short `maxAge`.
    

Using it from a page looks like any other data fetch, except you pass `Astro` through:

src/pages/blog/\[slug\].astro

```astro
---
import { executeQuery } from '~/lib/datocms/executeQuery';
import { BLOG_POST_QUERY } from './_graphql';

const data = await executeQuery(Astro, BLOG_POST_QUERY, {
  slug: Astro.params.slug,
});
---

<h1>{data.blogPost.title}</h1>
```

### Step 3: Make sure component-level tags aren't missed

This is the one non-obvious part of the integration, and it's worth understanding rather than just copying.

Astro streams HTML by default. The response object resolves as soon as the page *starts* rendering, and Astro applies the cache headers at that moment — but queries living inside a `Layout`, a `<Header />` or a `<Footer />` haven't run yet. Their tags get registered after the `Cache-Tag` header has already been serialized, so they're silently dropped. The symptom is nasty precisely because it's partial: the page caches, tag-based purging works for the page's own query, and updating a record that only appears in the footer never purges anything.

The fix is to buffer the HTML in middleware, which forces the full render — and therefore every query — to complete before the headers are applied:

src/middleware.js

```javascript
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
  let response = await next();

  const contentType = response.headers.get('content-type') || '';

  if (contentType.includes('text/html')) {
    // Astro applies the cache headers as soon as this middleware returns, but
    // with streaming the response resolves before the body is rendered — so any
    // executeQuery() inside Layout/Header/Footer would register its cache tags
    // too late. Buffering the body here forces the whole page to render (and
    // every query to run) before the tags are serialized.
    response = new Response(await response.arrayBuffer(), response);
  }

  return response;
});
```

You're giving up streaming for HTML responses. On a page that's about to be cached at the edge for a year, that's a very cheap trade: the buffering cost is paid once per cache miss, and every subsequent visitor is served by the CDN anyway.

### Step 4: Purge on content change

DatoCMS emits an [invalidation webhook](/docs/content-delivery-api/cache-tags-invalidation.md) listing exactly which tags are now stale. Your endpoint's whole job is to hand those tags to `cache.invalidate()`:

src/pages/api/invalidate-cache.js

```javascript
import { CACHE_INVALIDATION_WEBHOOK_SECRET } from 'astro:env/server';

// The webhook delivers a payload shaped like this:
//
//   {
//     entity_type: 'cda_cache_tags',
//     event_type: 'invalidate',
//     entity: {
//       id: 'cda_cache_tags',
//       type: 'cda_cache_tags',
//       attributes: { tags: ['8f2a1c', '3b91e7', ...] },
//     },
//   }

export const POST = async ({ request, cache }) => {
  if (
    request.headers.get('Authorization') !==
    `Bearer ${CACHE_INVALIDATION_WEBHOOK_SECRET}`
  ) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await request.json();
  const tags = body.entity.attributes.tags;

  if (!tags.length) {
    return Response.json({ error: 'Missing tags' }, { status: 400 });
  }

  await cache.invalidate({ tags });

  return Response.json({ success: true, invalidated: tags.length });
};
```

Then, in your DatoCMS project, go to **Settings \> Webhooks**, create a webhook pointing at `https://your-site.com/api/invalidate-cache`, add an `Authorization: Bearer <your-secret>` header, and subscribe it to the **Cache tags invalidation** event.

That's the entire loop. A record is published, DatoCMS computes which tags are affected, your endpoint purges them, and the next visitor to any page that touched that record gets a fresh render.

> [!PROTIP] Pro tip: Nothing here is route-aware
> Notice that no part of this integration knows which URLs exist, or which pages a record appears on. The tags carry that information implicitly — which is why this keeps working when you add pages, nest components, or restructure your routes.

### Verifying it works

Cloudflare reports the cache outcome in [`Cf-Cache-Status`](https://developers.cloudflare.com/workers/cache/debugging/):

Terminal window

```bash
curl -sI https://your-site.com/blog/hello-world | grep -i cf-cache-status


# cf-cache-status: MISS   ← first request


# cf-cache-status: HIT    ← second request, served without reaching your Worker
```

Now edit that record in DatoCMS and publish it. The next request should be a `MISS` again, with the new content — that transition is the whole integration working.

When it isn't:

-   **No** **`Cf-Cache-Status`** **at all** — Workers Cache isn't active: `cache.enabled` is missing from `wrangler.jsonc`, or Wrangler is older than 4.69.0.
-   **A** **`HIT`** **that survives publishing** — look at the webhook, not the caching. DatoCMS logs every delivery along with the response your endpoint returned, which separates "never fired" from "returned a 401".
    

Don't go looking for the tags themselves: Cloudflare [strips `Cache-Tag`](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/) before the response leaves the edge. To see what your app registered, echo `Astro.cache.tags` from the middleware, after the buffering step:

```javascript
// src/middleware.js — temporary, for debugging only
response.headers.set('X-Debug-Cache-Tags', context.cache.tags.join(','));
```

### Things to keep in mind

**Draft mode must not be cached.** If you've set up [draft mode](/docs/astro/accessing-draft-updated-content.md), draft responses are per-editor and must never reach a shared cache. The simplest guard is to skip tag registration entirely when drafts are on:

```javascript
if (!includeDrafts && cacheTags.length > 0) {
  context.cache.set({ maxAge: ONE_YEAR, tags: cacheTags });
}
```

Astro also gives you `Astro.cache.set(false)`, which disables caching for the current response outright — useful as a belt-and-braces measure in a middleware that already knows whether draft mode is active. Either way, pair it with an explicit `Cache-Control: private, no-store` on draft responses.

**Deploys don't invalidate anything.** Cache tags track *content* changes, not *code* changes. Ship a template tweak and the CDN will happily keep serving the old HTML for a year. Add a full purge to your deploy pipeline — on Cloudflare, an authenticated route is the easiest way:

```javascript
import { cache } from 'cloudflare:workers';

await cache.purge({ purgeEverything: true });
```

**Mind the tag budget.** A Content Delivery API response can carry up to [500 cache tags](/docs/content-delivery-api/cache-tags-format.md), and every query on a page contributes to the same set. Cloudflare has room to spare; other hosts don't (see below). A page that genuinely needs hundreds of tags is usually a page worth rethinking anyway — paginate the listing, or narrow the query.

### Deploying on Netlify or Vercel

The code doesn't change. Swap the adapter and its provider in `astro.config.mjs` and everything else — `executeQuery`, the middleware buffering, the webhook endpoint — stays exactly as written:

```javascript
// Netlify
import netlify from '@astrojs/netlify';
import { cacheNetlify } from '@astrojs/netlify/cache';
export default defineConfig({
  output: 'server',
  adapter: netlify(),
  cache: { provider: cacheNetlify() },
});

// Vercel
import vercel from '@astrojs/vercel';
import { cacheVercel } from '@astrojs/vercel/cache';
export default defineConfig({
  output: 'server',
  adapter: vercel(),
  cache: { provider: cacheVercel() },
});
```

What differs is underneath, and one difference is worth knowing before you commit:

-   **Cloudflare** — tags go out as `Cache-Tag`, purging goes through the Workers `cache.purge()` API. The header holds 16 KB, roughly 1,000 tags.
-   **Netlify** — tags go out as `Netlify-Cache-Tag`, purging through `purgeCache()` from `@netlify/functions`. The cap is 500 tags per response, exactly matching ours.
    
-   **Vercel** — tags go out as `Vercel-Cache-Tag`, purging through `invalidateByTag()` from `@vercel/functions`. The cap is **128 tags per response**.
    

Cloudflare and Netlify comfortably fit everything DatoCMS can return. Vercel caps a cached response at [128 tags](https://vercel.com/docs/caching/cdn-cache/purge), well below our 500, so a page whose queries collectively touch more than 128 records will overflow the budget. Most detail pages are nowhere near that ceiling; large listing pages can cross it without anyone noticing. Vercel doesn't document what happens to the excess, so if you're on Vercel with tag-heavy pages, verify the behaviour before relying on it.

Vercel's purge path is also chattier: the provider issues one `invalidateByTag()` call per tag, so a webhook delivery carrying a few hundred tags becomes a few hundred concurrent API calls inside one function invocation. If you're on Vercel and publishing in bulk, chunk the tags in your webhook handler rather than passing the whole array through at once.

> [!PROTIP] Pro tip: This is not a Next.js-versus-Astro difference
> The 128-tag ceiling belongs to Vercel's CDN, not to any framework. What Astro saves you on Vercel is the plumbing — tags still travel straight from our response header to `Vercel-Cache-Tag`, with no mapping layer in between — but the budget is the platform's, and it applies either way.

---

# React Router — React Router + DatoCMS Overview

Source [docs]: https://www.datocms.com/docs/react-router.md

React Router is the routing standard of the React ecosystem, and since version 7 it also ships a full-stack framework mode: server rendering, nested routes, data loading, actions and type-safe route modules, all built on top of Vite. It lets you get started without having to write much boilerplate code, and with a set of sane defaults from which you can build upon.

React Router fully supports edge functions and advanced caching mechanisms, and React Router projects can be deployed on many different hostings, such as [Netlify](https://netlify.com/), [Vercel](https://vercel.com/) and [Cloudflare Workers](https://workers.cloudflare.com/). See the [deploying guide](https://reactrouter.com/start/framework/deploying) for the full list of options.

DatoCMS is the perfect companion to React Router since it offers content, images, and videos on a globally-distributed CDN. With this combo, you can have an **infinitely scalable website, ready to handle prime-time TV traffic spikes, at a fraction of the regular cost.**

> [!NOTE] Coming from Remix?
> React Router v7 in framework mode is the direct continuation of Remix v2: same route modules, same `loader` and `action` functions, same progressive enhancement. The main difference is that imports moved from `@remix-run/*` to `react-router`. If you have an existing Remix v2 project, follow the official [upgrade guide](https://reactrouter.com/upgrading/remix).
> 
> Be aware that [Remix v3](https://remix.run/) is a different, React-less framework, with no migration path from Remix v2. This guide does not cover it.

Our [marketplace](https://www.datocms.com/marketplace/starters.md) features different demo projects you can learn from and get started easily. The following one is built with Remix, but every pattern in it applies to React Router as well:

### Fetching content from our GraphQL API

First, use the React Router wizard to set up a new project. Read more about your options on the [React Router docs](https://reactrouter.com/start/framework/installation).

Terminal window

```bash
npx create-react-router@latest
```

The way you fetch content from external sources in React Router is by exporting a `loader` function from your [route modules](https://reactrouter.com/start/framework/route-module). Whatever the loader returns is handed over to the React component through its `loaderData` prop:

app/routes/home.tsx

```tsx
import type { Route } from './+types/home';

export async function loader() {
  return { foo: 'bar' };
}

export default function Homepage({ loaderData }: Route.ComponentProps) {
  const { foo } = loaderData;

  // ...
}
```

Route modules are then wired up in `app/routes.ts`, which is where you declare the URL each of them responds to:

app/routes.ts

```typescript
import { type RouteConfig, index } from '@react-router/dev/routes';

export default [index('routes/home.tsx')] satisfies RouteConfig;
```

Inside the `loader` function, we can use any Node.JS GraphQL client (or HTTP client, really) to fetch content from the [Content Delivery API](/docs/content-delivery-api.md) of DatoCMS.

Let's start by installing `@datocms/cda-client`, a lightweight, TypeScript-ready package that offers various helpers around the native Fetch API to perform GraphQL requests towards [DatoCMS Content Delivery API](/docs/content-delivery-api/api-endpoints.md):

Terminal window

```bash
npm install --save @datocms/cda-client
```

> [!PROTIP] Pro tip: Top 5 JavaScript GraphQL Client Libraries
> Our `@datocms/cda-client` is not the only option. This [blog post](https://www.datocms.com/blog/best-javascript-graphql-clients.md) ranks the best JavaScript GraphQL client libraries, helping you choose the right tool based on your project’s specific needs and ensuring efficient and optimized GraphQL data fetching.

We can now create a function we can use in all of our components that need to fetch content from DatoCMS: Create a new directory called `lib` inside `app`, and inside of it, add a file called `datocms.js`:

app/lib/datocms.js

```javascript
import { executeQuery } from '@datocms/cda-client';

export const load = (query, options) => {
  return executeQuery(query, {
    ...options,
    token: process.env.DATOCMS_READONLY_TOKEN,
    environment: process.env.DATOCMS_ENVIRONMENT,
  });
}
```

We want to store inside environment variables both the API token and the name of the DatoCMS environment we want to fetch content from to hide them from the code, and so that we'll be able to modify them later without touching the code. React Router runs on [Vite](https://vite.dev/guide/env-and-mode), so a `.env` file at the root of your project is all you need during development. Loaders and actions only ever run on the server, so they can safely read those secrets from `process.env`.

To create an API token for a DatoCMS project, go to `Settings > API Tokens` section of your DatoCMS backend. Make sure to only give it permissions to access the (read-only) Content Delivery API.

(Video content)

It's time to use our function in a real page! Open up `app/routes/home.tsx`, which is the route that renders the homepage, and define the `loader` function and a basic page component:

```tsx
import type { Route } from './+types/home';
import { load } from '~/lib/datocms';

const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  posts: allBlogPosts(first: $limit) {
    title
  }
}`;

export async function loader() {
  return load(HOMEPAGE_QUERY, {
    variables: { limit: 10 },
  });
}

export default function Home({ loaderData }: Route.ComponentProps) {
  const { posts } = loaderData;

  return <div>{JSON.stringify(posts, null, 2)}</div>;
}
```

The `HOMEPAGE_QUERY` is the GraphQL query, and of course it depends on the models available in your specific DatoCMS project. You can learn everything you need regarding how to build GraphQL queries on our [Content Delivery API documentation](/docs/content-delivery-api.md).

For more information on what to do next, we recommend reading the next sections of this integration guide!

---

# React Router — Managing images

Source [docs]: https://www.datocms.com/docs/react-router/managing-images.md

One of the major advantages of using DatoCMS instead of any other CMS is its [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images), which will return pre-computed image attributes that will help you to set up **responsive, lazy loaded images** in your frontend without any effort.

Our solution allows you to show beautiful image placeholders (LQIP) in base64 format, without any additional request to be made by the browser or server:

(Video content)

Inside your project, install the [`react-datocms`](https://github.com/datocms/react-datocms) package. It offers many functionality that will help us build our React Router website:

Terminal window

```bash
npm i react-datocms --save
```

Inside any of your pages, you can now feed the data coming from a [`responsiveImage` query](/docs/content-delivery-api/images-and-videos.md#responsive-images) directly into the `<Image />` component that this package offers.

```tsx
import type { Route } from './+types/home';
import { load } from '~/lib/datocms';
import { Image } from 'react-datocms';

const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  posts: allBlogPosts(first: $limit) {
    id
    title
    coverImage {
      responsiveImage(imgixParams: { fit: crop, w: 300, h: 300, auto: format }) {
        srcSet
        webpSrcSet
        sizes
        src
        width
        height
        aspectRatio
        alt
        title
        base64
      }
    }
  }
}`;

export async function loader() {
  return load(HOMEPAGE_QUERY, {
    variables: { limit: 10 },
  });
}

export default function Home({ loaderData }: Route.ComponentProps) {
  const { posts } = loaderData;

  return (
    <div>
      {posts.map(blogPost => (
        <article key={blogPost.id}>
          <Image data={blogPost.coverImage.responsiveImage} />
          <h6>{blogPost.title}</h6>
        </article>
      ))}
    </div>
  );
}
```

With so little code, the image component:

-   Generates multiple smaller images so smartphones and tablets don’t download desktop-sized images;
-   Efficiently lazy-loads images to speed initial page load and save precious bandwidth;
    
-   Holds the image position so your page doesn’t jump while images load;
-   Uses blur-up techniques to show a preview of the image while it's still loading;

---

# React Router — Displaying videos

Source [docs]: https://www.datocms.com/docs/react-router/displaying-videos.md

> [!PROTIP] Pro tip: Start with our how-to guides first
> If you're new to hosting videos on DatoCMS, we recommend first starting with our tutorials:
> 
> -   How to upload videos: [Videos and Video Optimizations](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)
>     
> -   Why you should use HLS Streaming via Mux: [How to stream videos efficiently: Raw MP4 Downloads vs HLS Streaming](/docs/streaming-videos/how-to-stream-videos-efficiently.md)
>     
> 
> Then, this page provides framework-specific playback advice using our helper components. Read on when you're ready!

One of the advantages of using DatoCMS instead of other content management systems is its `video` query, which will return **pre-computed video attributes that will help you display videos in your frontend without any additional manipulation**.

To make it easy to offer optimized, progressive videos on your projects, we offer a package called [`react-datocms`](https://github.com/datocms/react-datocms) that exposes a `<VideoPlayer />` component and pairs perfectly with the video query.

To take advantage of it, install the [`react-datocms`](https://github.com/datocms/react-datocms) package:

Terminal window

```bash
npm install react-datocms
```

Then, inside your page, feed content coming from a `video` query directly into the `<VideoPlayer />` component:

```tsx
import type { Route } from './+types/home';
import { load } from '~/lib/datocms';
import { VideoPlayer } from 'react-datocms';

const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  posts: allBlogPosts(first: $limit) {
    id
    title
    coverVideo {
      video {
        muxPlaybackId
        title
        width
        height
        blurUpThumb
      }
    }
  }
}`;

export async function loader() {
  return load(HOMEPAGE_QUERY, {
    variables: { limit: 10 },
  });
}

export default function Home({ loaderData }: Route.ComponentProps) {
  const { posts } = loaderData;

  return (
    <div>
      {posts.map(blogPost => (
        <article key={blogPost.id}>
          <VideoPlayer data={blogPost.coverVideo.video} />
          <h6>{blogPost.title}</h6>
        </article>
      ))}
    </div>
  );
}
```

---

# React Router — Structured Text fields

Source [docs]: https://www.datocms.com/docs/react-router/structured-text-fields.md

Rich-text in DatoCMS is stored in [Structured Text](/docs/content-modelling/structured-text.md) fields, as it offers many advantages over regular HTML.

There's a lot to be said about Structured Text and the extensibility of it, but for now let's just say that it returns content in a particular [JSON format called `dast`](/docs/structured-text/dast.md) which resembles this example:

```json
{
  "schema": "dast",
  "document": {
    "type": "root",
    "children": [
      {
        "type": "heading",
        "level": 1,
        "children": [
          {
            "type": "span",
            "value": "Hello world!"
          }
        ]
      }
    ]
  }
}
```

To make it easy to render Structured Text inside your React Router projects, we released a package called [`react-datocms`](https://github.com/datocms/react-datocms) that exposes a `<StructuredText />` component that performs all the tedious work for you.

To take advantage of it, install the [`react-datocms`](https://github.com/datocms/react-datocms) package if you haven't already:

Terminal window

```bash
npm i --save react-datocms
```

Then, inside your page, make a [GraphQL query to fetch a Structured Text field](/docs/content-delivery-api/structured-text-fields.md), and feed the result to the `data` prop of a `<StructuredText />` component:

```tsx
import type { Route } from './+types/home';
import { load } from '~/lib/datocms';
import { StructuredText } from 'react-datocms';

const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  posts: allBlogPosts(first: $limit) {
    id
    title
    content {
      value
    }
  }
}`;

export async function loader() {
  return load(HOMEPAGE_QUERY, {
    variables: { limit: 10 },
  });
}

export default function Home({ loaderData }: Route.ComponentProps) {
  const { posts } = loaderData;

  return (
    <div>
      {posts.map(blogPost => (
        <article key={blogPost.id}>
          <h6>{blogPost.title}</h6>
          <StructuredText data={blogPost.content} />
        </article>
      ))}
    </div>
  );
}
```

## Rendering special nodes

Other than “regular” formatting nodes — paragraphs, headings, lists, etc. — Structured Text documents can contain four special types of node:

-   [`itemLink` nodes](/docs/structured-text/dast.md#itemLink) are just like regular HTML hyperlinks, but point to other records instead of URLs;
-   [`inlineItem` nodes](/docs/structured-text/dast.md#inlineItem) let you directly embed a reference to a record in-between regular text;
    
-   [`block` nodes](/docs/structured-text/dast.md#block) let you embed a DatoCMS block record in-between regular paragraphs;
-   [`inlineBlock` nodes](/docs/structured-text/dast.md#block) lets you embed a DatoCMS block record in-between regular text;
    

If a Structured Text document contains one of these nodes, then we need to change the GraphQL query, so that we also fetch all the records and blocks it references.

As an example, if the field can link to other `Blog posts`, and can embed blocks of type `Image block` and and `Mention block`, then the query should change like this:

```jsx
const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  posts: allBlogPosts(first: $limit) {
    id
    title
    content {
      value
      blocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on ImageBlockRecord {
          image { url alt }
        }
      }
      inlineBlocks {
        ... on RecordInterface {
          id
          __typename
        }
        ... on MentionBlockRecord {
          username
        }
      }
      links {
        ... on RecordInterface {
          id
          __typename
        }
        ... on BlogPostRecord {
          slug
          title
        }
      }
    }
  }
}`;
```

We also need to tell `<StructuredText />` how you want such nodes to be rendered:

```jsx
return (
  <StructuredText
    data={blogPost.content}
    renderInlineRecord={({ record }) => {
      switch (record.__typename) {
        case "BlogPostRecord":
          return <a href={`/blog/${record.slug}`}>{record.title}</a>;
        default:
          return null;
      }
    }}
    renderLinkToRecord={({ record, children }) => {
      switch (record.__typename) {
        case "BlogPostRecord":
          return <a href={`/blog/${record.slug}`}>{children}</a>;
        default:
          return null;
      }
    }}
    renderBlock={({ record }) => {
      switch (record.__typename) {
        case "ImageBlockRecord":
          return <img src={record.image.url} alt={record.image.alt} />;
        default:
          return null;
      }
    }}
    renderInlineBlock={({ record }) => {
      switch (record.__typename) {
        case "MentionBlockRecord":
          return <code>@{record.username}</code>;
        default:
          return null;
      }
    }}
  />
);
```

---

# React Router — Adding SEO to pages

Source [docs]: https://www.datocms.com/docs/react-router/seo-management.md

Similarly to what we offer with [responsive images](/docs/next-js/managing-images.md), our GraphQL API also offers a way to fetch [**pre-computed meta tags**](/docs/content-delivery-api/seo-and-favicon.md) **based on the content you insert inside DatoCMS**.

Here's a sample of the meta tags you can automatically generate. It includes meta tags for SEO, social share and website favicons:

```html
<title>DatoCMS Blog - DatoCMS</title>
<meta property="og:title" content="DatoCMS Blog" />
<meta name="twitter:title" content="DatoCMS Blog" />
<meta name="description" content="Lorem ipsum..." />
<meta property="og:description" content="Lorem ipsum..." />
<meta name="twitter:description" content="Lorem ipsum..." />
<meta property="og:image" content="https://www.datocms-assets.com/..." />
<meta property="og:image:width" content="2482" />
<meta property="og:image:height" content="1572" />
<meta name="twitter:image" content="https://www.datocms-assets.com/..." />
<meta property="og:locale" content="en" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="DatoCMS" />
<meta property="article:modified_time" content="2020-03-06T15:07:14Z" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@datocms" />
<link sizes="16x16" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="32x32" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="96x96" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
<link sizes="192x192" type="image/png" rel="icon" href="https://www.datocms-assets.com/..." />
```

You can easily include all this information inside your React Router app with the help of our [`react-datocms`](https://github.com/datocms/react-datocms) package, so make sure to install it, if you've not already done it:

Terminal window

```bash
npm i --save react-datocms
```

### Adding SEO and social share meta tags

With React Router, you can specify meta tags for a page using the [`meta` export](https://reactrouter.com/start/framework/route-module):

```tsx
export function meta({ data }: Route.MetaArgs) {
  return [
    { title: 'Something cool' },
    {
      name: 'description',
      content: 'This becomes the nice preview on search results.',
    },
  ];
}
```

With DatoCMS you can feed content coming from a [`_seoMetaTags` query](/docs/content-delivery-api/seo-and-favicon.md) directly into React Router by using the `toRemixMeta` function, which generates an array of meta descriptors compatible with React Router's `meta` export. The helper keeps the Remix name for backwards compatibility, but its output is exactly what React Router expects:

app/routes/home.tsx

```tsx
import type { Route } from './+types/home';
import { toRemixMeta } from 'react-datocms';
import { load } from '~/lib/datocms';

const HOMEPAGE_QUERY = `
  {
    blog {
      seo: _seoMetaTags {
        attributes
        content
        tag
      }
    }
  }`;

export async function loader() {
  return load(HOMEPAGE_QUERY);
}

export function meta({ data }: Route.MetaArgs) {
  return toRemixMeta(data.blog.seo);
}

export default function Home() {
  // ...
}
```

### Adding favicon links and meta tags

If you want to add all the `link` and `meta` tags needed to generate favicons for your website, you can use the `renderMetaTags` helper along with the `faviconMetaTags` GraphQL query:

> [!WARNING] Why not using the links export?
> React Router offers a [`links`](https://reactrouter.com/start/framework/route-module) export to define which `<link>` elements to add to the page, but it doesn't receive any loader data, so you cannot use it to render favicon meta tags! The best way to render them is using `renderMetaTags` in your root component, like in the example.

app/root.tsx

```tsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useRouteLoaderData,
} from 'react-router';
import { renderMetaTags } from 'react-datocms';
import { load } from '~/lib/datocms';

export async function loader() {
  return load(`
    {
      site: _site {
        favicon: faviconMetaTags(variants: [icon, msApplication, appleTouchIcon]) {
          attributes
          content
          tag
        }
      }
    }
  `);
}

export function Layout({ children }: { children: React.ReactNode }) {
  // Layout is also rendered by error boundaries, when no loader data is available
  const data = useRouteLoaderData('root');

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
        {data && renderMetaTags(data.site.favicon)}
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
```

Want to know more about SEO customization in DatoCMS? Check out this video tutorial:

[

(Image content)

Working with and customizing SEO Fields

Play video »

](https://youtu.be/WjF10isSjS0)

---

# React Router — Setting up a preview mode

Source [docs]: https://www.datocms.com/docs/react-router/setting-up-a-preview-mode.md

Most often than not, editors of a DatoCMS project will find very beneficial to have a preview of how the changes they are making to ie. an article will be rendered inside the final website.

With React Router you can easily add a "preview mode" to your production website. With that, requests coming from editors will add a special header — `X-Include-Drafts` — that [returns content that is not yet published](/docs/content-delivery-api/api-endpoints.md#include-drafts).

### Step 1: Create resource routes to turn Preview mode on/off

First, we need to create a couple of [resource routes](https://reactrouter.com/how-to/resource-routes) to enable/disable Preview Mode. We're going to use React Router's [built-in session management](https://reactrouter.com/api/utils/createCookieSessionStorage) to store a cookie inside the browser of the visitor.

First step is to actually create the session manager. Create a new file under `app/sessions.ts`:

app/sessions.ts

```typescript
import { createCookieSessionStorage } from 'react-router';

const { getSession, commitSession, destroySession } = createCookieSessionStorage({
  cookie: {
    name: '__session',
    maxAge: 604_800,
    path: '/',
  },
});

export { getSession, commitSession, destroySession };
```

Now we can use it inside a new resource route under `app/routes/preview-start.ts`, that we can call to turn on the preview mode:

app/routes/preview-start.ts

```typescript
import { redirect } from 'react-router';
import { getSession, commitSession } from '~/sessions';
import type { Route } from './+types/preview-start';

export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get('Cookie'));

  session.set('preview', 'yes');

  return redirect('/', {
    headers: {
      'Set-Cookie': await commitSession(session),
    },
  });
}
```

Similarly, we also need to create a route under `app/routes/preview-stop.ts`, to turn preview mode off:

app/routes/preview-stop.ts

```typescript
import { redirect } from 'react-router';
import { getSession, commitSession } from '~/sessions';
import type { Route } from './+types/preview-stop';

export async function action({ request }: Route.ActionArgs) {
  const session = await getSession(request.headers.get('Cookie'));

  session.unset('preview');

  return redirect('/', {
    headers: {
      'Set-Cookie': await commitSession(session),
    },
  });
}
```

Both routes only export an `action`, with no component to render, so we declare them in `app/routes.ts` as any other route:

app/routes.ts

```typescript
import { type RouteConfig, index, route } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('preview/start', 'routes/preview-start.ts'),
  route('preview/stop', 'routes/preview-stop.ts'),
] satisfies RouteConfig;
```

We can now tweak the `app/root.tsx` file to add to every page a button to toggle the preview on and off:

app/root.tsx

```tsx
import {
  Form,
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useRouteLoaderData,
} from 'react-router';
import { getSession } from '~/sessions';
import type { Route } from './+types/root';

export async function loader({ request }: Route.LoaderArgs) {
  const session = await getSession(request.headers.get('Cookie'));
  return { previewEnabled: session.has('preview') };
}

export function Layout({ children }: { children: React.ReactNode }) {
  const data = useRouteLoaderData('root');

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {data?.previewEnabled ? (
          <Form method="post" action="/preview/stop">
            <button>Exit preview mode</button>
          </Form>
        ) : (
          <Form method="post" action="/preview/start">
            <button>Enter preview mode</button>
          </Form>
        )}
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}
```

### Step 2: Fetch non-published content with X-Include-Drafts

Now every `loader` can know from the session if the visitor is currently in preview mode by looking at the `request` object.

If that's the case, we can run the same query, but passing the `X-Include-Drafts` header, which [returns the records at their **latest version available**](/docs/content-delivery-api/api-endpoints.md#include-drafts) instead of the one that's currently set as published:

app/routes/home.tsx

```tsx
import type { Route } from './+types/home';
import { load } from '~/lib/datocms';
import { getSession } from '~/sessions';

const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  posts: allBlogPosts(first: $limit) {
    title
  }
}`;

export async function loader({ request }: Route.LoaderArgs) {
  const session = await getSession(request.headers.get('Cookie'));

  return load(HOMEPAGE_QUERY, {
    variables: { limit: 10 },
    includeDrafts: session.has('preview'),
  });
}

export default function Home({ loaderData }: Route.ComponentProps) {
  const { posts } = loaderData;

  return <div>{JSON.stringify(posts, null, 2)}</div>;
}
```

---

# React Router — Real-time updates

Source [docs]: https://www.datocms.com/docs/react-router/real-time-updates.md

Live updates can be extremely useful both for content editors and the regular visitors of your app/website:

-   Content-editors in Preview Mode can **see drafts directly in the production website**, without having to refresh the page;
-   Visitors can **immediately see new content as it gets published**, allowing all kinds of real-time interactions with your website/app (ie. live-news coverage).
    

(Video content)

Inside a React Router project, it's extremely easy to use our [Real-time Updates API](/docs/real-time-updates-api.md) to perform such changes, as it only involves adding a React hook to your page components.

### How to use the `useQuerySubscription` hook

The [`react-datocms`](https://github.com/datocms/react-datocms#live-real-time-updates) package exposes a [`useQuerySubscription` hook](https://github.com/datocms/react-datocms#live-real-time-updates) that makes it trivial to update any React Router page in real-time.

The hook works by streaming any changes present to the response of a GraphQL query directly to the browser, and it a `loader` responsibility to prepare an object compatible with the options of the hook itself.

The following code shows a complete example that **activates real-time updates for any visitor** of your website:

app/routes/blog-post.tsx

```tsx
import type { Route } from './+types/blog-post';
import { useQuerySubscription } from 'react-datocms';
import { load } from '~/lib/datocms';

const BLOG_POST_QUERY = `query HomePage {
  blogPost {
    title
  }
}`;

export async function loader() {
  return {
    subscription: {
      query: BLOG_POST_QUERY,
      initialData: await load(BLOG_POST_QUERY),
      token: process.env.DATOCMS_READONLY_TOKEN,
      environment: process.env.DATOCMS_ENVIRONMENT,
    },
  };
}

export default function BlogPost({ loaderData }: Route.ComponentProps) {
  const { data, error, status } = useQuerySubscription(loaderData.subscription);

  const statusMessage = {
    connecting: 'Connecting to DatoCMS...',
    connected: 'Connected to DatoCMS, receiving live updates!',
    closed: 'Connection closed',
  };

  return (
    <div>
      <p>Connection status: {statusMessage[status]}</p>
      {error && (
        <div>
          <h1>Error: {error.code}</h1>
          <div>{error.message}</div>
          {error.response && (
            <pre>{JSON.stringify(error.response, null, 2)}</pre>
          )}
        </div>
      )}
      {data && (
        <div>{JSON.stringify(data, null, 2)}</div>
      )}
    </div>
  );
}
```

### Preview Mode + `useQuerySubscription`

Another common scenario is being able to activate real-time updates of draft content **only for content editors** that are signed-in to the website via [Preview Mode](/docs/react-router/setting-up-a-preview-mode.md):

(Video content)

In this case, you don't want to expose your API token or pass down additional arguments to regular users, so:

-   Make sure to pass the `includeDrafts: true` option only if Preview Mode is active (that is, if the `preview` key is set on the session), so that only content editors [will see draft content;](/docs/content-delivery-api/api-endpoints.md)
-   If Preview Mode is off, fill in the `subscription` object returned by the loader with just `initialData` and `enabled: false` options, without any additional clutter.
    

Here's an example snippet:

app/routes/blog-post.tsx

```tsx
import type { Route } from './+types/blog-post';
import { load } from '~/lib/datocms';
import { getSession } from '~/sessions';

const BLOG_POST_QUERY = `query HomePage {
  blogPost {
    title
  }
}`;

export async function loader({ request }: Route.LoaderArgs) {
  const session = await getSession(request.headers.get('Cookie'));
  const previewModeActive = session.has('preview');

  const initialData = await load(BLOG_POST_QUERY, {
    includeDrafts: previewModeActive,
  });

  return {
    subscription: previewModeActive
      ? {
          query: BLOG_POST_QUERY,
          initialData,
          token: process.env.DATOCMS_READONLY_TOKEN,
          environment: process.env.DATOCMS_ENVIRONMENT,
          includeDrafts: true,
        }
      : {
          enabled: false,
          initialData,
        },
  };
}
```

---

# Agency Partner Program — Agency Partner Program Overview

Source [docs]: https://www.datocms.com/docs/agency-partner-program.md

The DatoCMS Partner Program is designed to offer your agency the support it needs to expand your business, while using a headless CMS that works best for your customers.

## Enrollment requirements

To be a part of the program, is necessary to comply with a few requirements. You can learn more in the [Enrollment](/docs/agency-partner-program/enrollment-requirements.md) section of this guide.

## Benefits

Enrolling in the program gives your agency access to exclusive advantages and benefits:

-   **Special plans and discounts, both for you and your clients:** have access to customized plans, designed specifically for the needs of agencies, with the ability to [make them also available to your clients' accounts](/docs/agency-partner-program/partners-dashboard.md#enabling-special-plans-to-clients);
-   **Automatic access to your clients' projects:** assign your staff members a [special "developer" role](/docs/agency-partner-program/partners-dashboard.md#developer-and-projects-manager-roles), allowing them to have [full access to all your — and your clients' — projects](/docs/agency-partner-program/partners-dashboard.md#automatic-access-to-your-clients-projects), even when they reside on separate accounts;
    
-   **Dedicated partner account manager:** gain access to constant support from our Partner Team to tackle any questions you (or your customers) might have;
-   **Co-marketing opportunities:** our marketing relies on real success stories — and we know that our Partners will provide some great ones. We’ll promote your projects, create case studies and articles, and feature your logo on our website;
    
-   **DatoCMS partner listing:** we’ll get you in front of new potential clients by featuring your agency and your projects as part of our [Partners](https://www.datocms.com/partners.md) page. Teams in need of development resources go there to find the right level of support for their projects.
    

You can explore the effect these benefits have on your DatoCMS dashboard in the [Partners dashboard](/docs/agency-partner-program/partners-dashboard.md) section of this guide.

> [!POSITIVE] Connecting your agency to your clients' accounts
> Many of the benefits that you will be able to pass on to your customers once you're part of the program come through the concept of [agency mandates](/docs/agency-partner-program/agency-mandates.md), which will be covered in the next section of this guide.

---

# Agency Partner Program — Clients and Agency mandates

Source [docs]: https://www.datocms.com/docs/agency-partner-program/agency-mandates.md

As a DatoCMS Agency Partner, you can connect your organization with your clients' organizations by sending them an "agency mandate" request. A mandate represents a voluntary link between two organizations, that of your agency and the client's, which allows the agency to carry out certain operations on behalf of the client.

## What do I get by adding a new client?

Agency mandates unlock **special plans and discounts** on your client's organization, and allows your staff to **enter all your client's projects with full privileges**, without using any additional collaborator seat!

-   From the **Projects** section, [you will be able to see your clients projects](/docs/agency-partner-program/partners-dashboard.md#automatic-access-to-your-clients-projects), and enter them with full privileges;
-   From the **Clients** section, you will be able to see the current plan active in the client's organization, and [enable special discounts and plans](/docs/agency-partner-program/partners-dashboard.md#enabling-special-plans-to-clients) on their end:
    

(Video content)

## Adding a new client

Once your agency is part of our program, a new **Clients** section will be available from your organization's dashboard. From there, you will be able to add new clients in two different ways.

#### Case 1: The client does not have a DatoCMS organization yet

If the client does not yet have an organization, you can create one for them to simplify their onboarding process:

(Video content)

The procedure will create a new organization for the client, of which you will be the owner. An agency mandate will also be automatically created for the new organization.

> [!POSITIVE] Delegate your responsibilities by inviting the client!
> We recommend that you also invite the client to the organization, so that after the client assumes control of the new organization, you can step back and let them take over. Even if you leave your client organization, you and your entire team will still have full access to the client's projects through your own agency organization.

#### Case 2: The client already has their own DatoCMS organization

In this case, you can associate your agency organization with the one that the client already owns by sending the client a mandate request.

> [!NOTE] Only organizations can become clients!
> If the client is managing projects from a personal account, they need to convert it into an organization first. [It only takes a couple of clicks.](/docs/general-concepts/organizations-and-accounts.md#converting-a-personal-account-into-a-new-organization)

The procedure is straightforward, but you need to know the client's organization ID:

(Video content)

You should request the organization's ID directly from your client, who can locate it in the Settings area of their organization.

(Video content)

As soon as you request an agency mandate, the owners of your client's organization will get an email alert about it. They can approve the request by clicking the link in the email, or by going to the Settingsarea of their organization:

(Video content)

You will be immediately notified by e-mail of the acceptance or decline of the request.

## Revoking the mandate

At any time both parties — your agency or the client — can revoke the agency mandate. If this happens, the agency will no longer be able to access the client's projects, and any special discount/plan enabled on the client organization by the agency will no longer be available.

---

# Agency Partner Program — Partners dashboard

Source [docs]: https://www.datocms.com/docs/agency-partner-program/partners-dashboard.md

Once you become part of our Agency Partner Program, a number of new features become available in your organization dashboard. Let's see them in detail.

### **Automatic access to your clients' projects**

Once you [add a new client to your agency organization](/docs/agency-partner-program/agency-mandates.md), your staff members will have complete access to the client's projects. This eliminates the need to individually invite each staff member to the client's organization or occupy additional collaborator seats.

In the **Projects** section of your dashboard, you can easily distinguish your client's projects from the ones you own, as they're marked with the name of the client organization:

(Video content)

### Enabling special plans to clients

Once you [set up an agency mandate](/docs/agency-partner-program/agency-mandates.md) with a client, you can also unlock exclusive pricing opportunities for them:

-   If they purchase one of the [public DatoCMS plans](https://www.datocms.com/pricing.md), **a special discount gets automatically applied during the checkout process**, without having to insert any referral code. Be aware that the discount applies only to the regular plan price, and not on monthly overages (extra collaborators, API calls, traffic, etc.);
-   You can also **enable special plans on their organization**. These plans are only available if you are enrolled in the Agency Partner Program.
    

In the **Clients** section of your dashboard, you can monitor the current plan active on all your client's organizations, and activate special plans.

Once activated, special plans are immediately available for purchase on the client's end:

(Video content)

### Developer and Projects Manager roles

In addition to the regular [Owner and Viewer roles](/docs/general-concepts/organizations-and-accounts.md#organization-members) available to every organization, two new roles can be applied to the members of your organization:

As the name suggests, **Developer** can be an useful role for developers/content creators of your staff, so that you don't have to use collaborators seats on every project for them. They can enter all the projects available in the organization — either owned by your org, or by one of your clients [connected with a mandate](/docs/agency-partner-program/agency-mandates.md) — with full privilege, but cannot perform any action inside the organization itself (ie. they cannot create new projects, delete existing ones, manage members, etc.)

**Projects managers** have the same priviledges of Developers, but have also complete control over the projects owned by the organization. They can create new projects, manage settings of existing projects, and even delete them.

The table below summarizes the available authorizations for each role:

| Permission | Owner | Projects Manager | Developer | Viewer |
| --- | --- | --- | --- | --- |
| Read-only access to everything | ✅ | ✅ | ✅ | ✅ |
| Enter all projects with full proviledges (client's projects included) | ✅ | ✅ | ✅ |  |
| Create/edit/delete projects | ✅ | ✅ |  |  |
| Transfer projects | ✅ | ✅ |  |  |
| Manage members/roles | ✅ |  |  |  |
| Manage plan and billing | ✅ |  |  |  |
| Any other action | ✅ |  |  |  |

---

# Agency Partner Program — Enrollment requirements

Source [docs]: https://www.datocms.com/docs/agency-partner-program/enrollment-requirements.md

After joining the partner program, it is necessary to comply with certain requirements in order to remain a member.

> [!WARNING] Compliance deadline is 3 months away from enrollment!
> As a rule of thumb, **requirements must be met within 3 months of joining the partner program**. Periodic emails will be sent to the owners of your organisation to remind you to meet the requirements in time.
> 
> Exceptions to this deadline may be allowed in special cases. Consult our Partners Team at least one week before the final deadline if you need an extension!

Let's see what those requirements are in detail.

### 1\. An organization is needed

To be a part of the program, you cannot use a personal DatoCMS account to manage your projects, but a proper [organization](/docs/general-concepts/organizations-and-accounts.md#organizations).

Organizations are a much better fit for an agency with multiple staff members in any case, even if they don't want to get in the program. If you're managing projects from a personal account, you can [convert it into an organization](/docs/general-concepts/organizations-and-accounts.md#converting-a-personal-account-into-a-new-organization) in just a couple of clicks.

### 2\. A paid DatoCMS plan must be active

On your agency organization, you can choose either to activate one of the [public plans](https://www.datocms.com/pricing.md), or one of the special plans available to agencies.

If it is normally not your agency that pays for DatoCMS, but your customers, then at least one of the [clients for which you have a mandate](/docs/agency-partner-program/agency-mandates.md) must be on a paid plan. Again, they can either choose a discounted public plan, or one of the special plans [you can enable on their organization](/docs/agency-partner-program/partners-dashboard.md#enabling-special-plans-to-clients).

### 3\. Your agency profile must be published

Once you are selected as eligible, go to your organization's dashboard and click on "Agency profile" to create your agency's entry on DatoCMS website. Fill in all the details about your agency, and, when you are ready, change the workflow stage from *Edit* to *Request Review from DatoCMS*. After our team approval, your agency page will be published on DatoCMS's website! ([example](https://www.datocms.com/partners/cantiere-creativo.md))

### 4\. At least one case study must be presented

We require our partners to prepare a showcase of one representative project they made using DatoCMS ([example](https://www.datocms.com/partners/lait-aps/showcase/mette-munk.md)).

Once approved by our team, it will be then published on our marketing website.

> [!POSITIVE] The more, the marrier 😉
> Needless to say, many of our partners choose to publish more than one case study, to better present their work and expertise to visitors to our site. We suggest you to do so too, but at least one case study is necessary.

Throughout your stay in the partner program, you can keep your profile up-to-date, and edit or add new case studies at any time. In fact, you are strongly advised to do so! Any changes you make to the content post-publication, will require an explicit approval step by our team, so that we can verify the appropriateness of the changes made.

[Learn how to manage your agency profile and showcase your projects.](/docs/agency-partner-program/public-profile-and-case-studies.md)

### What happens if I exit the program?

If you request us to exit the Agency Partner Program, or due to non-compliance with the minimum requirements for membership, the following effects will occur:

-   Any active [agency mandate](/docs/agency-partner-program/agency-mandates.md) will be revoked;
-   You will no longer be able to [access your clients' projects](/docs/agency-partner-program/partners-dashboard.md#automatic-access-to-your-clients-projects) from your organization;
    
-   It will no longer be possible for your organization, or those of your clients, to access the Partner Program's special discounts and plans;
-   The [Developer and Projects Manager roles](https://www.datocms.com/partner-program.md#developer-and-projects-manager-roles) will no longer be available in your organization. If any members were using them, they will be assigned to the Viewer role;
    
-   Your agency profile and any published case study present in our marketing website will be removed.

---

# Agency Partner Program — Public Profile and Case studies

Source [docs]: https://www.datocms.com/docs/agency-partner-program/public-profile-and-case-studies.md

#### Create your records

Once you're part of the program, a new "**Agency Profile**"link is available in your dashboard. By clicking on it, you will enter **a special DatoCMS project** where you'll be able to manage your agency profile and case studies.

> [!POSITIVE] It will be our responsibility to share your profile!
> Please be aware that we may feature your quotes, profile, or projects on our social media channels, newsletter, or for promotional purposes.

The process of submitting the profile and case studies for review will be guided by contextual help:

(Video content)

Throughout your stay in the partner program, you can keep your profile up-to-date, and edit or add new case studies at any time. In fact, you are strongly advised to do so! Any changes you make to the content post-publication, will require an explicit approval step by our team, so that we can verify the appropriateness of the changes made.

## Preview your draft content

From the moment you save your record for the first time, you can get a glimpse of the final outcome of your pages. In the right-hand menu of the record page, there's a link to preview the draft version or you can even have a full responsive preview from the Sidebar Panel. You can easily edit, save, and view the results in real-time.

(Video content)

## Request for review to go live

Due to security concerns, we cannot allow you to publish any content on our website without a review from our team. That's why we utilize Workflows to enable editing and requesting reviews.

When you're satisfied with your content, change the status from **Edit** to **Request a Review from DatoCMS**. Our team will be notified, and you'll receive a notification when your content goes live.

(Image content)

Please note that we never alter your content. If anything appears to be non-compliant or suspicious, we will get in touch with you as soon as possible.

## Leave a quote, if you wish

On your profile page, there's a dedicated section for adding quotes. If you choose to share your thoughts, they will be instantly published on our [customers' page.](https://www.datocms.com/wall.md)

---

# Plans, pricing and billing — Pricing Overview

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing.md

All projects on DatoCMS start from the free plan and can be upgraded to a paid plan directly from the [Account dashboard](https://dashboard.datocms.com/).

The differences between plans, including features and all the available resources, are listed in detail on the [pricing page](https://www.datocms.com/pricing.md).

Limits, features, and resources of public paid plans may change in the future, but active subscriptions will remain on the plan you chose unless you decide to switch to a newer plan yourself. Free plan usage limits and resources may change unilaterally instead.

We don't want to trick anyone into buying more expensive plans that they have planned for, so if you are about to buy and plans changed meanwhile, please [contact support](https://www.datocms.com/support.md) and we'll help you out.

### Changing plans

Plan changes can be performed at any time, and take effect immediately.

We prorate the price when you change plans, so you are only billed for the cost of the new plan less the remaining unused amount from your current plan.

In case of a downgrade, prorated credits will be created, with a part used to pay the new invoice, and the remaining credit balance will be available for future use.

All plans have access to the bulk of DatoCMS features, with the Enterprise plan having additional features, dedicated to larger teams that need a higher focus on security and volumes. You can see all the features [in our dedicated page](https://www.datocms.com/features.md).

---

# Plans, pricing and billing — Billing and pricing

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/billing-and-pricing.md

When you enroll in a monthly plan, you are billed for the first month up-front and then again on the same date each month moving forward until you cancel. When you enroll in an annual plan, you are billed for the first year up-front and then again on the same date each year moving forward until you cancel.

Projects in monthly plans follow the completion date of the billing profile. You can view this information from your dashboard. These projects are invoiced and charged along with any overage accrued during the previous month.

### Overage billing

While your subscription renewal follows a monthly or yearly schedule from the date in which you started, the overages are reset on the 1st day of the month. Going over the monthly quota of resources incur an overage charge as outlined in your plan. Overages are not invoiced immediately though. To leave some room for manual intervention we issue invoices on the second working day of the month, so we can still do manual adjustments or check if necessary. Also we have some rules in place to minimize the number of payments that we process and documents that we generate. The rules are:

-   overages are billed only if they are more than €100, otherwise they will be added as "unbilled charges" that you might see in your dashboard
-   unbilled charges are then added automatically to the following subscription invoice, or they are billed when they go over the above threshold
    
-   on the invoice that you will receive for the overages, we'll show the date in which the overages are computed, which is always on the month following the one on which the overages are computed. For example if you see Oct 2nd it refers to the overages of September.
-   if you have existing credit, for example due to a plan change that generates credit, and the overages are fully paid by the credits, then the invoice is generated immediately even if below the €100 threshold
    

### Plan adjustments

When you go over the plan limits for features like models, collaborators or others that you have control over (e.g. not traffic, API calls, video), then we don't immediately issue an invoice for that.

The changes are computed every 60 minutes to avoid having multiple transactions and documents when changing limits. Say for example that you are adding two collaborators one after the other, we try to do a single plan change instead of two.

Similarly to what happens for overages, if the payment is covered by existing credit, then the change is done immediately, otherwise we add the amount to the unbilled charges until it reaches the €100 threshold or a new subscription invoice is generated. The CMS warns editors before an action pushes them over the limits included in their plan, so the change isn't a surprise on the invoice.

---

# Plans, pricing and billing — Payment failures and billing notifications

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/payment-failures-and-billing-notifications.md

In case of payment failures, we will notify you by email, and automatically retry the payment **4 times over the next 21 days**. If payment still has not gone through after these attempts, and you have not contacted us, the project will be **temporarily deactivated** until you are able to complete payment. At any time, you can log in to your dashboard, enter new payment information, and tell our system to retry billing.

Project deactivation will only happen if the subscription payment itself fails, not if an overage payment fails. This is done to prevent unexpected overages from blocking your account. This is especially helpful for customers on an annual subscription, whose credit cards might've expired halfway through a subscription term.

### Notification types & recipients

In detail, this is how we handle notifications:

-   **Invoices**: All invoices are sent exclusively to the **billing email** associated with the account or organization.
-   **Failed Overage Payments**: Notifications regarding failed payments for overage charges are also sent solely to the **billing email**. Failed overage payments will not cause project deactivation.
    
-   **Critical Payment Issues**: If there is a payment issue with the subscription plan (not overages) that could lead to the **cancellation of your subscription and project deactivation**, we send notifications to the **billing email** and **project/organization owners**\*. These are sent as soon as payment fails, giving you time to resolve the issue before service is impacted.
-   **Subscription Deactivation & Reactivation**: If critical payment issues are not resolved in a timely manner, our system will automatically pause the subscription and deactivate your projects. These deactivation confirmation emails are sent to the **billing email** and **project/organization owners**\*. They will also get another email once billing is resolved and projects are reactivated.
    

| Notification Type | Billing Email | Project & Org Owner(s)\* | Org Viewers | Collaborators in Projects |
| --- | --- | --- | --- | --- |
| Invoices | ✅ | ❌ | ❌ | ❌ |
| Failed Overage Payments | ✅ | ❌ | ❌ | ❌ |
| Critical Payment Issues | ✅ | ✅ | ❌ | ❌ |
| Subscription Deactivation & Reactivation | ✅ | ✅ | ❌ | ❌ |

\*In larger organizations, only the first 50 owners (to join the org) will receive email notifications.

---

# Plans, pricing and billing — Cancellations and refunds

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/cancellations-and-refunds.md

You may downgrade or cancel your DatoCMS subscription at any time.

## Downgrading vs Canceling

Downgrading a project (to the Free plan) allows you to keep it active in our systems, as long as it fits under resource limits and the account owner [logs in at least once a year](/docs/plans-pricing-and-billing/free-developer-plan-limits-and-deactivations.md).

Canceling a paid subscription **immediately** **suspends** the project and stops further billing, but the project will be deleted after 90 days. Unused subscription time is converted into account credits.

## How to downgrade to the Free plan

To downgrade your subscription from any paid plan to the Free plan, you must first ensure all your projects, and your account as a whole, fits within the [free plan limits](https://www.datocms.com/pricing.md#free-plan-details).

How to downgrade from a paid plan to the free plan:

1.  Make sure your team knows this is happening, and that everyone has [backed up any projects and assets](/docs/import-and-export/export-data.md) they wish to keep to their own storage.
    
2.  Delete records, models, projects, etc. until your account is under the free plan resource limits. The Plan and Billing section of the [Dashboard](https://dashboard.datocms.com/) will show you a per-project breakdown of resource usage.
    
3.  From that same section, click the **Change plan** button to downgrade to the Free plan. If you are now within the free limits, the plan change will go through successfully. Otherwise, the system will stop the change and warn you.
    

## How to cancel your DatoCMS subscription

To cancel your subscription at any time during a billing cycle:

1.  Go to the Plan and Billing section of your [Dashboard](https://dashboard.datocms.com/).
    
2.  Click the **Cancel subscription** button and confirm.
    

Once you confirm the cancellation:

-   All projects owned by the canceled account will be **immediately** **suspended**. No further edits or access (including via API or asset CDN) will be possible, except for project deletion via the Dashboard.
-   Any pending overage charges will be tallied and immediately charged to the credit card on file.
    
-   Further billing will stop.
-   You will have 89 days from the time of cancellation to resubscribe and reactivate your projects.
    
-   **At 90 days, all projects owned by your account will be deleted.** You will get several email notifications to remind you about this before it happens.
    

## **Refunds**

Accounts can request a refund within 14 days of initial purchase or renewal by [contacting our support team](https://www.datocms.com/support.md).

Refunds are issued to the original payment method, and may take up to 10 business days for processing.

If you cancel a subscription with time remaining, your project will be immediately deactivated and the remaining time will be prorated and converted into account credit instead. We do not provide refunds for this credit, but you can use it for other DatoCMS projects or charges, or to reactivate your project later.

Account credits are good for 18 months and expire after that.

Please note that **projects under a canceled billing plan are deleted at 90 days**. If you wish to keep the project active without a paid plan, you must reduce its resource usage and change your account to the Free plan instead (see above).

### Enterprise Plans

Enterprise agreements are subject to their own, individualized contracts. Please speak to your account representative for details.

---

# Plans, pricing and billing — Credit card change

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/credit-card-change.md

If your card expires, or if you need to change the credit card attached to your billing profile for any other reason, go to your billing profile page:

(Image content)

and then click on the "Change credit card" button.

(Image content)

By doing this, you won't need to transfer your project to a separate account, and you will not need to start a new billing cycle. This will simply start charging a different card and keep everything else unchanged.

---

# Plans, pricing and billing — How overages are managed

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/overcharges-on-api-and-bandwidth.md

**Last Updated: May 6, 2026.**

Different DatoCMS plans have different monthly usage limits, as documented on our [Pricing](https://www.datocms.com/pricing.md) page.

Please note that monthly limits are reset on the 1st of each month. Details about overage billing are found in the [billing section](/docs/plans-pricing-and-billing/billing-and-pricing.md).

### Monitoring data usage in Dashboard

If you have multiple projects, or you want to check data usage without entering a specific project, you can do so from our Dashboard.

#### Overage status

Located in the "Plan and Billing" page, this panel serves as a health indicator for your data consumption, covering bandwidth (traffic) usage, API calls, and video streaming.

(Image content)

Here's what the different status colors mean:

-   🟢 **So far, so good**: You are within your plan's limits.
-   🟡 **Your attention is needed**: You are approaching your plan's limits and may face overage charges if exceeded.
    
-   🔴 **Limits exceeded**: You have surpassed your plan's limits, and additional usage will be billed.
    

If your plan is in a "yellow" or "red" state, we'll also give you a heads up by showing a notification badge next to the Plan and Billing link in the navigation.

(Image content)

For a detailed view, simply click on the "Overage Status" panel. This action will redirect you to the Data Usage page.

#### Data usage

This page provides detailed charts displaying your usage trends over time.

(Video content)

Here's what you can expect:

-   **Usage Segmentation:** View data by resource type, either aggregated or by individual project.
-   **Time Comparison:** Compare current usage with the previous month.
    
-   **Historical Data:** Access your data history to review long-term trends.
-   **Forecasting:** Get predictions of end-of-month usage to proactively manage your resources and prevent overages.
    

If you have the appropriate permissions, you'll find direct links to the **Project Usage Page** for a granular look at each project's consumption.

### Monitoring project usage

It is possible to check the day-by-day consumption of a project from the "Project usages" section that is part of the "Project settings" area. This section presents various graphs and detailed tables for the current and previous month.

(Image content)

### Exceeding your plan's limits

When you reach a usage limit on a free plan project, the service will be temporarily disabled until the beginning of the following calendar month.

For projects that are part of a paid plan, exceeding the limits does not lead to an interruption of service, but will result in an additional fee commensurate with the excess use.

The current overage rates are documented on our [Pricing](https://www.datocms.com/pricing.md) page.

Details about overage billing are found in the [billing section](/docs/plans-pricing-and-billing/billing-and-pricing.md).

Changing your plan increases or decreases the monthly limits of DatoCMS in real-time.

### Progressive notifications

Our system helps you monitor resource usage and avoid unexpected interruptions or charges on paid plans.

You receive progressive notifications as you approach or exceed your limits. Free plans are blocked once the limit is reached, while paid plans can continue with additional charges. Notifications are sent at 50%, 80%, and 100% for free plans, and at 80%, 100%, and at the end of month for paid plans. You can always check your usage in the dashboard, and we aim to keep you informed without overwhelming you with alerts.

### 4K Video Streaming

If you upload a video with a resolution that exceeds 1080p. and have the "4K Video Streaming" feature enabled on your plan, the video player will be able to serve higher resolution streaming for your viewers (up to 4K/2160p).

**Seconds of videos delivered in a resolution higher than 1080p will be charged with a 3x multiplier on DatoCMS due to the higher costs that Mux applies in this case.** That is, if a visitor streams 30 seconds of a video in 4K, DatoCMS will count the view as 30s x 3 = 90s.

The video player selects the best video resolution based both on the density of the screen and the actual size of the player in the page, so you will only pay for the actual streaming time that occurred at resolutions over 1080p. In other words, displaying higher resolution videos on a small-sized player won't lead to extra streaming costs.

To cut down on your delivery expenses, you can stop providing streaming for a video above a certain resolution by using a `max_resolution` query parameter to the regular Playback URL. This modifies the resolution options available for the player to select from:

```none
https://stream.mux.com/{PLAYBACK_ID}.m3u8?max_resolution=1080p
```

The `max_resolution` parameter can be set to `720p`, `1080p`, `1440p`, or `2160p`.

> [!NOTE] 4K Video Streaming is available for Enterprise plans
> As of today, 4K video streaming is only available upon request on Enterprise plans. Therefore, for the vast majority of customers, we will not apply multiplier will ever be applied to the seconds of video streaming delivered.

---

# Plans, pricing and billing — Transfer project

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/transfer.md

Let's have a look at the *Danger zone* that you'll find at the bottom of the project description in your dashboard if you are the project owner:

(Image content)

We'll focusing on the transfer ownership section. The duplicate and delete will be covered in the next section of the guide.

### Transfer project ownership

Transferring a project is useful **when you want to start a new billing cycle** under a different account. In this case you can go ahead and click "Transfer".

This will let you input the email address of the destination account.

Conversely, the receiving account will receive an email and find a popup at the top of their dashboard:

(Image content)

On accepting the project you'll be charged for the extra project if you are exceeding the limits of the free "Developer" plan.

If the former owner is left with any unused credit it will show in their billing profile and those will be used automatically on any new invoice.

**Credit cannot be transferred from one billing profile to another.**

### Change ownership retaining the billing cycle

If you don't need to start a new billing cycle, as for example you need to change ownership of the project inside the same company, you don't need to use the project transfer feature.

The best way to achieve this is by simply changing the email address of the account owner.

If the destination email is already a DatoCMS user, the destination account needs to change email address too, or delete the account first as email cannot be duplicated.

### Transfer of a project on a legacy per-site pricing

In case you need to transfer a project that is still on a legacy per-site pricing, you can safely migrate it to an account or organization that is on the new pricing structure.

If you instead need to transfer the project to another account still on a per-site pricing, [please contact support](https://www.datocms.com/support.md).

---

# Plans, pricing and billing — Duplicate or delete project

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/duplicate-delete.md

In the *Danger zone* that you can find at the bottom of the project description in your dashboard if you are the project owner, you'll see the actions to duplicate or delete a project.

(Image content)

### Duplicate project

Duplicating a project is an easy (and fast) way to make a copy of it, either for a backup, or to use as a [blueprint project](/docs/scripting-migrations/keeping-multiple-datocms-projects-in-sync.md).

However, please keep in mind that **only the primary environment is duplicated**. [Sandbox environments](/docs/general-concepts/primary-and-sandbox-environments.md) are NOT copied.

When duplicating a project you'll be asked if you want to duplicate the schema only or also the data by using this toggle:

(Image content)

### Delete project

When deleting the project we immediately delete all your content from our database. We have a small window of time in which we can retrieve backups, but after that all is gone, so be extremely careful when performing this action.

---

# Plans, pricing and billing — Migrating from a legacy plan to current pricing

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/migration-to-the-new-pricing.md

> [!NOTE] We support both old and new plans!
> DatoCMS [pricing plans](https://www.datocms.com/pricing.md) change from time to time, usually on an annual basis. However, existing customers are generally grandfathered into their existing plans as long their accounts remain active. These "legacy" plans are not available to newer customers.
> 
> At any time, existing customers on legacy plans have the *option* of switching to more recent plans — but only if they want to! Sometimes, newer features (like [Visual Editing](/docs/visual-editing.md)) are only available on more recent plans. On the other hand, older plans may have different resource limits that work better for a particular customer. It's entirely up to them!

If you are on a discontinued, legacy DatoCMS pricing plan and you want to switch to the current Professional plan, here are the steps you should take:

-   Temporarily change the email address of your current DatoCMS account to a variation like *email+old@yourdomain.com* (if your email provider supports "[plus addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing)"), or to another email address under control.
-   Create a **new** DatoCMS account with your original email, e.g., *email@yourdomain.com.*
    
-   Buy the new Professional plan under the *new* account — even though it's empty right now.
-   [Transfer your existing project(s)](/docs/plans-pricing-and-billing/transfer.md) from your old account to the new one you just created
    
-   [Get in touch](https://www.datocms.com/support.md) with our support team to process a refund of the remaining credit of your old account. This manual processing is required because our billing system cannot automatically process the different account types. No worries though, our team is happy to help!
    

If you have any questions, please don't hesitate to [contact our support team](https://www.datocms.com/support.md).

---

# Plans, pricing and billing — Free plan limits & deactivations

Source [docs]: https://www.datocms.com/docs/plans-pricing-and-billing/free-developer-plan-limits-and-deactivations.md

> [!POSITIVE] Paid plans are not affected
> **Everything on this page applies only to projects and accounts on the Free plan.** If you're on a paid plan of any type, no need to worry about these!

## Production use of the Free plan is discouraged

Our [Free plan](https://www.datocms.com/pricing.md#free-plan-details) is primarily intended for testing and prototyping DatoCMS projects before production deployment on a paid plan. Although the free plan can also be used for small projects that see minimal real-world traffic, we generally do not recommend this approach because the free plan is subject to deactivations.

### Why was my project deactivated?

Projects on the free plan are subject to two types of automatic deactivation or deletion:

-   Its monthly resource limits have been reached, resulting in **temporary project suspension** until the 1st of the following month
-   The project owner has not logged in for 12 months (365 days), resulting in **project and account deletion** after several email warnings and a cautionary deactivation
    

**Paid plans in good standing are not subject to these deactivations or deletions.** Instead, paid plans incur pay-as-you-go resource overage charges according to the details of each plan.

## Monthly resource limits & temporary project suspension

If a Free Plan account hits one of the monthly resource limits (bandwidth, API calls, etc.), **all of its projects will be suspended for the remainder of the calendar month.**

Suspension means that all admin UI, API, and asset CDN access will be disabled. You will not able to edit your projects, and any frontends connected to them will no longer be able to query their APIs or access their images, videos, and files over the CDN.

The limits will automatically reset on the 1st day of the next month and access will be automatically restored then as well.

### What if I need to restore access to a deactivated project?

The easiest and quickest way to restore access to a deactivated project is to upgrade the owner's account to any paid plan. On a monthly plan, you can downgrade back to a Free plan at any time, as long as you remain within the limits of the Free plan.

This means you can upgrade to a paid plan for just a month if you are expecting a traffic spike, then downgrade again after that.

In exceptional circumstances, if your project was deactivated due to factors outside your control and a paid upgrade isn't an option, please reach out to [DatoCMS Support](https://www.datocms.com/support.md) for help.

## Inactive account deletion

To help conserve resources, we periodically clean up unused accounts. **Free accounts that have not logged in to their project or dashboard within the most recent 12 months (365 days) are subject to automatic deletion**.

### Email warnings

We send several email warnings to the account owner\* before deletion:

-   **365 days** of no login activity: First email warning sent to account owner.
-   **+30 days** later: **All projects are deactivated** (not deleted yet). Second email warning sent.
    
-   **+60 days** later: Third email warning sent.
-   **+61 days** later: Fourth and final email warning sent.
    
-   **+62 days** later: **Account and all of its projects are deleted.**
    

**\*** These emails are sent only to the account owner on file, not any of the project collaborators.

> [!WARNING] Make sure the account account owner email is up to date!
> Please note that email warnings are sent only to the project/account owner, not to any of the collaborators inside a project.
> 
> The account owner email is the only contact information we have for important notices like this. Please make sure you keep it updated and active.

## **Preventing inactive account deletion**

To prevent deletion, the **account** **owner** can simply login to their dashboard or a specific project at any time prior to deletion. The login must be from the **account owner**, not any of the collaborators inside a project.

### What if the account owner cannot be reached or changed?

In some situations (such as team turnover, independent contractors, or defunct web agencies), a DatoCMS project may still be actively used by collaborators inside a project, but its account owner on file with us has not logged in for more than a year (see above for detailed timeline).

If a project is still in use, but the account owner is no longer active and cannot be reached, please contact [DatoCMS Support](https://www.datocms.com/support.md) for help. We may be able to manually process an account ownership change for you, but please be prepared to provide formal documentation, such as an official letter on company letterhead or some other proof of ownership change.

---

# Enterprise integration — Amazon AWS S3 Storage

Source [docs]: https://www.datocms.com/marketplace/enterprise/aws-s3.md

### Use a custom S3 bucket that you own to store all the assets you upload to your DatoCMS project

DatoCMS allows you to use your own AWS and Imgix accounts to store your project assets. This allows to be in total control of your data, and to offer a custom CDN domain for your assets — which, by default is `www.datocms-assets.com` for every project.

## How to activate custom AWS storage for your DatoCMS project

To store your DatoCMS assets in a custom AWS S3 bucket please follow these steps:

### Create a new bucket

Login to the [AWS console](https://console.aws.amazon.com/) and create a new S3 bucket.

Make sure to configure "Object ownership" settings like this:

(Image content)

and "Block Public Access" settings like this:

(Image content)

Make sure to add the following CORS configuration to the bucket:

```json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["GET", "POST", "PUT"],
    "AllowedOrigins": ["*"],
    "ExposeHeaders": []
  }
]
```

Create a IAM key for DatoCMS with the following permissions:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "s3:DeleteObject",
        "s3:ListBucket",
        "s3:GetObject",
        "s3:GetBucketLocation",
        "s3:PutObject",
        "s3:PutObjectAcl"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::your-bucket-name",
        "arn:aws:s3:::your-bucket-name/*"
      ]
    },
    {
      "Action": [
        "rekognition:DetectLabels",
        "rekognition:DetectModerationLabels"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
```

We recommend you to create a stricter IAM key for Imgix as they won't need to upload objects:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation",
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::your-bucket-name",
        "arn:aws:s3:::your-bucket-name/*"
      ]
    }
  ]
}
```

### Create an Imgix source

Go to [Imgix](https://www.imgix.com/) and create a new account. Create a new source, and link it to the S3 bucket you just created.

(Image content)

#### Adding a custom domain

If you're not satisfied with the default Imgix subdomain (ie. [https://your-source.imgix.net](https://your-source.imgix.net/)) you can add a custom domain to the Imgix source, then configure your domain DNS settings so that its CNAME record points to `your-source.imgix.net`:

(Image content)

#### Enable HTTPS for the Imgix source

DatoCMS requires HTTPS for custom domains. There are two different ways you can enable it. The first one is to request an HTTP certificate to Imgix. From the [Imgix documentation](https://docs.imgix.com/setup/creating-sources/advanced-settings):

> By default, you will only be able to use the custom subdomain with http. Using https requires an SSL certificate through our CDN partner and incurs additional fees—please [contact Imgix Support](mailto:support@imgix.com) to set this up.

Alternatively, to get HTTPS for free, you can use Cloudflare on top of Imgix. This is a cheaper alternative, but requires changing your original domain nameservers to the Cloudflare nameservers, which is something you might not want, and [might have some impacts in the way assets are returned](https://docs.imgix.com/best-practices/cdn-guidelines).

### Send request for custom uploads to DatoCMS support

Once everything is ready, send an email to [support@datocms.com](mailto:support@datocms.com) and request the change. These are the information we'll ask you for:

-   Your S3 bucket name (`my-bucket-name`) and region (ie. `eu-west-1`)
-   Your IAM key ID and secret
    
-   The Imgix domain (ie. `your-source.imgix.net` or `assets.superduper.com`)
    

Together we'll schedule a maintenance window where we'll transfer every assets already uploaded to your Project to the new S3 bucket, and enable the custom domain.

From then on all new assets you upload will be stored in your AWS S3 bucket, and will be available from your custom Imgix domain.

---

# Enterprise integration — Azure Blob Storage

Source [docs]: https://www.datocms.com/marketplace/enterprise/azure-blob-storage.md

### Use an Azure Blob Storage container of your choice to store all the assets you upload to your DatoCMS project

DatoCMS allows you to use your own Azure and Imgix accounts to store your project assets. This allows to be in total control of your data, and to offer a custom CDN domain for your assets — which, by default is `www.datocms-assets.com` for every project.

## How to activate a custom Azure Blob Storage for your DatoCMS project

To store your DatoCMS assets in a custom Azure Blob Storage container please follow these steps:

### Create a new container

Inside your Microsoft Azure dashboard:

1.  Enter the **Storage accounts** service
    
2.  Select the storage account where you want to create a new container (or create a new one)
    
3.  Enter the **Data Storage \> Containers** section
    
4.  Create a new container
    

(Image content)

### Enable CORS on storage account

Inside your Microsoft Azure dashboard:

1.  Enter the **Storage accounts** service
    
2.  Select the storage account where you want to create a new container
    
3.  Enter the **Settings \> Resource sharing (CORS)** section
    

Add the following settings, then press **Save**:

-   Allowed origins: `*`
-   Allowed methods: `PUT`
    
-   Allowed headers: `content-type,x-ms-blob-type`
    

(Image content)

### Create a new Application

Inside your Microsoft Azure dashboard:

1.  Enter the **Microsoft Entra ID** service
    
2.  Enter the **Manage \> App Registrations** section
    
3.  Press the **New registration** button
    
4.  Give a name to the new application (ie. **DatoCMS Custom Storage**)
    
5.  Press the **Register** button
    
6.  Enter the **Manage \> Certificate & secrets** section
    
7.  Select the **Client secrets** tab
    
8.  Press the **New client secret** button
    
9.  Specify **730 days (24 months)** in the **Expires** field
    
10.  Press the **Add** button
     
11.  Copy the **Value** of the secret
     

(Image content)

Now go back to the **Overview** section, and copy the **Directory (tenant) ID** and **Application (client) ID**:

(Image content)

### Create a custom role

Inside your Microsoft Azure dashboard:

1.  Enter the **Storage accounts** service
    
2.  Select the storage account where you want to create a new container
    
3.  Enter the **Access Control (IAM)** section
    
4.  Select the **Roles** tab
    
5.  Search the "Storage Blob Data Reader" role, and select **Clone**
    

(Image content)

Inside the **Create a custom role** modal flow, edit the role to apply the following characteristics, making sure to replace the `<ID>`, `<SUBSCRIPTION_ID>` and `<STORAGE_ACCOUNT_ID>` with the correct values:

```json
{
    "id": "<ID>",
    "properties": {
        "roleName": "Storage Blob Data Reader and Writer",
        "description": "",
        "assignableScopes": [
            "/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/DatoCMS-Integration-Test/providers/Microsoft.Storage/storageAccounts/<STORAGE_ACCOUNT_ID>"
        ],
        "permissions": [
            {
                "actions": [
                    "Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action"
                ],
                "notActions": [],
                "dataActions": [
                    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read",
                    "Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write"
                ],
                "notDataActions": []
            }
        ]
    }
}
```

### Assign the role to the application

Inside your Microsoft Azure dashboard:

1.  Enter the **Storage accounts** service
    
2.  Select the storage account where you want to create a new container
    
3.  Enter the **Access Control (IAM)** section
    
4.  Select **Add \> Add role assignment**
    
5.  Under the **Role** tab, select the newly created **Storage Blob Data Reader and Writer** role
    
6.  **Select Next**
    
7.  Under the **Members** tab, press **Select members**, and choose the **DatoCMS Custom Storage** application
    
8.  Under the **Conditions** tab, press **Add condition**
    
9.  Under **Editor type**, select **Code**
    

Now inside the code editor, **paste the following code**, making sure to replace`<CONTAINER_NAME>` with the name of your container:

```plaintext
(
 (
  !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read'})
  AND
  !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write'})
 )
 OR
 (
  @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringEquals '<CONTAINER_NAME>'
 )
```

**Select Save**, then **Review + assign**.

### Create an Imgix source

Go to [Imgix](https://www.imgix.com/) and create a new account. Create a new source, and link it to the Azure container you just created.

(Image content)

#### Adding a custom domain

If you're not satisfied with the default Imgix subdomain (ie. [https://your-source.imgix.net](https://your-source.imgix.net/)) you can add a custom domain to the Imgix source, then configure your domain DNS settings so that its CNAME record points to `your-source.imgix.net`:

(Image content)

#### Enable HTTPS for the Imgix source

DatoCMS requires HTTPS for custom domains. There are two different ways you can enable it. The first one is to request an HTTP certificate to Imgix. From the [Imgix documentation](https://docs.imgix.com/setup/creating-sources/advanced-settings#custom-domains):

> By default, you will only be able to use the custom subdomain with http. Using https requires an SSL certificate through our CDN partner and incurs additional fees—please [contact Imgix Support](mailto:support@imgix.com) to set this up.

Alternatively, to get HTTPS for free, you can use Cloudflare on top of Imgix. This is a cheaper alternative, but requires changing your original domain nameservers to the Cloudflare nameservers, which is something you might not want, and [might have some impacts in the way assets are returned](https://docs.imgix.com/best-practices/cdn-guidelines).

### Send request for custom uploads to DatoCMS support

Once everything is ready, send an email to [support@datocms.com](mailto:support@datocms.com) and request the change. These are the information we'll ask you for:

-   The name of your **Azure Storage Account**
-   The name of your **Container**
    
-   The **Directory (tenant) ID**, **Application (client) ID** and **Client Secret Value** of your Azure Application
-   The Imgix domain (ie. `your-source.imgix.net` or `assets.superduper.com`)
    

Together we'll schedule a maintenance window where we'll transfer every assets already uploaded to your Project to the new Azure container, and enable the custom domain.

From then on all new assets you upload will be stored in your Azure Blob Storage container, and will be available from your custom Imgix domain.

---

# Enterprise integration — Google Cloud Storage

Source [docs]: https://www.datocms.com/marketplace/enterprise/google-cloud-storage.md

### Use a custom storage bucket that you own to store all the assets you upload to your DatoCMS project

DatoCMS allows you to use your own Google Cloud and Imgix accounts to store your project assets. This allows to be in total control of your data, and to offer a custom CDN domain for your assets — which, by default is `www.datocms-assets.com` for every project.

## How to activate custom Google Cloud Storage for your DatoCMS project

To store your DatoCMS assets in a custom Google Cloud Storage bucket please follow these steps:

### Create a new bucket

Login to the [Google Cloud Console](https://console.cloud.google.com/storage/browser) and create a new Storage bucket inside one of your projects. Make sure you select **Uniform** access control:

(Image content)

#### Create an interoperable key

Open the [Cloud Storage Settings page](https://console.cloud.google.com/storage/settings) and select the **Interoperability** tab:

(Image content)

Inside this page:

-   Click the **Set PROJECT-ID as default project** button. If the project is already the default project, you will see *PROJECT-ID is your default project for interoperable access*.
-   Under the *User account HMAC* section, click on the **Create a key** button.
    

Copy **Access key** and **Secret** for later use.

#### Set up CORS policies on the bucket

The first step is to obtain a temporary access token:

-   Enter the [OAuth 2.0 playground](https://developers.google.com/oauthplayground/)
-   Under the *Select & authorize APIs* pane select `https://www.googleapis.com/auth/devstorage.full_control` (it's under *Cloud Storage API v1*)
    
-   Click the **Authorize APIs** button and follow the authentication process
-   When the OAuth flow completes, copy the **Access token**
    

Perform the following API request to set up proper CORS settings to the bucket, replacing `<ACCESS-TOKEN>` with the actual access token we just obtained and `<BUCKET-NAME>` with your bucket name:

Terminal window

```bash
curl 'https://storage.googleapis.com/storage/v1/b/<BUCKET-NAME>?fields=cors' \
      -X PATCH \
      -H 'Authorization: Bearer <ACCESS-TOKEN>' \
      -H 'Content-Type: application/json' \
      --data-binary '{ "cors": [{ "maxAgeSeconds": "3600", "method": ["GET", "POST", "PUT"], "origin": ["*"], "responseHeader":["Content-Type"] }] }'
```

#### Create a Service Account key and associate it to the bucket

Enter the [Service accounts page](https://console.cloud.google.com/iam-admin/serviceaccounts) and create a new service account:

(Image content)

Skip the *Grant this service account access to project* step. In the last step, press the **Create key** button, and select the JSON type:

(Image content)

Download the JSON key file and store it for later use.

Now return to your bucket in the **Permissions** tab and add **Storage Object Viewer** role to the service account just created.

(Image content)

As the last step, enable the [Cloud Vision API](https://console.cloud.google.com/apis/library/vision.googleapis.com) on your project:

(Image content)

### Create an Imgix source

Go to [Imgix](https://www.imgix.com/) and create a new account. Create a new source, and link it to the Cloud Storage bucket you just created.

(Image content)

#### Adding a custom domain

If you're not satisfied with the default Imgix subdomain (ie. [https://your-source.imgix.net](https://your-source.imgix.net/)) you can add a custom domain to the Imgix source, then configure your domain DNS settings so that its CNAME record points to `your-source.imgix.net`:

(Image content)

#### Enable HTTPS for the Imgix source

DatoCMS requires HTTPS for custom domains. There are two different ways you can enable it. The first one is to request an HTTP certificate to Imgix. From the [Imgix documentation](https://docs.imgix.com/setup/creating-sources/advanced-settings):

> By default, you will only be able to use the custom subdomain with http. Using https requires an SSL certificate through our CDN partner and incurs additional fees—please [contact Imgix Support](mailto:support@imgix.com) to set this up.

Alternatively, to get HTTPS for free, you can use Cloudflare on top of Imgix. This is a cheaper alternative, but requires changing your original domain nameservers to the Cloudflare nameservers, which is something you might not want, and [might have some impacts in the way assets are returned](https://docs.imgix.com/best-practices/cdn-guidelines).

### Send request for custom domain assets to DatoCMS support

Once everything is ready, send an email to [support@datocms.com](mailto:support@datocms.com) and request the change. These are the information we'll ask you for:

-   Your Cloud Storage bucket name (`my-bucket-name`)
-   Your interoperable **Access key** and **Secret**
    
-   Your Service account JSON key file
-   The Imgix domain (ie. `your-source.imgix.net` or `assets.superduper.com`)
    

Together we'll schedule a maintenance window where we'll transfer every assets already uploaded to your Project to the new bucket, and enable the custom domain.

---

# Enterprise integration — Cloudflare R2 Storage

Source [docs]: https://www.datocms.com/marketplace/enterprise/cloudflare-r2.md

### Use a custom R2 bucket of your choice to store all the assets you upload to your DatoCMS project

DatoCMS allows you to use your own Cloudflare and Imgix accounts to store your project assets. This allows to be in total control of your data, and to offer a custom CDN domain for your assets — which, by default is `www.datocms-assets.com` for every project.

## How to activate custom Cloudflare R2 storage for your DatoCMS project

To store your DatoCMS assets in a custom Cloudflare R2 bucket please follow these steps:

### Create a new bucket

To create a new R2 bucket from the Cloudflare dashboard:

1.  Log in to the [Cloudflare dashboard](https://dash.cloudflare.com/) and select **R2**
    
2.  Select **Create bucket**
    
3.  Enter a name for the bucket
    
4.  At this time, Imgix does not support the "Specify jurisdiction" option, so choose the "Automatic" option for Location, possibly specifying a location hint.
    
5.  Select **Create bucket**
    

(Image content)

### Configure CORS Policy

Enter the bucket settings, and in the **CORS Policy** section, select **Edit CORS Policy**, and paste the following code:

```json
[
  {
    "AllowedOrigins": [
      "*"
    ],
    "AllowedMethods": [
      "PUT"
    ],
    "AllowedHeaders": [
      "Content-Type"
    ]
  }
]
```

### Create an R2 API Token

To create a new R2 API Token from the Cloudflare dashboard:

1.  Log in to the [Cloudflare dashboard](https://dash.cloudflare.com/) and select **R2**
    
2.  Select **Manage R2 API Tokens**
    
3.  **Click** the **Create API Token** button
    
4.  Specify a name for the token (ie. "DatoCMS Read/Write")
    
5.  Pick the **"Object Read & Write"** option under **Permissions**
    
6.  Under **Specify bucket(s)**, select **Apply to specific buckets only** and select the newly created bucket
    
7.  **Under TTL**, select **"Forever"**
    
8.  **Click Create API Token**
    

**In the next page, make sure to copy the** following values (you'll need to pass these info to DatoCMS Support later in the process):

-   Access Key ID
-   Secret Access Key
    
-   Endpoint for S3 clients
    

### Create an Imgix source

Go to [Imgix](https://www.imgix.com/) and create a new account. Create a new source, and link it to the R2 bucket you just created.

(Image content)

#### Adding a custom domain

If you're not satisfied with the default Imgix subdomain (ie. [https://your-source.imgix.net](https://your-source.imgix.net/)) you can add a custom domain to the Imgix source, then configure your domain DNS settings so that its CNAME record points to `your-source.imgix.net`:

(Image content)

#### Enable HTTPS for the Imgix source

DatoCMS requires HTTPS for custom domains. There are two different ways you can enable it. The first one is to request an HTTP certificate to Imgix. From the [Imgix documentation](https://docs.imgix.com/setup/creating-sources/advanced-settings#custom-domains):

> By default, you will only be able to use the custom subdomain with http. Using https requires an SSL certificate through our CDN partner and incurs additional fees—please [contact Imgix Support](mailto:support@imgix.com) to set this up.

Alternatively, to get HTTPS for free, you can use Cloudflare on top of Imgix. This is a cheaper alternative, but requires changing your original domain nameservers to the Cloudflare nameservers, which is something you might not want, and [might have some impacts in the way assets are returned](https://docs.imgix.com/best-practices/cdn-guidelines).

### Send request for custom uploads to DatoCMS support

Once everything is ready, send an email to [support@datocms.com](mailto:support@datocms.com) and request the change. These are the information we'll ask you for:

-   Your bucket name (`my-bucket-name`)
-   Your Access Key ID, Secret Access Key and Endpoint for S3 clients
    
-   The Imgix domain (ie. `your-source.imgix.net` or `assets.superduper.com`)
    

Together we'll schedule a maintenance window where we'll transfer every assets already uploaded to your Project to the new R2 bucket, and enable the custom domain.

From then on all new assets you upload will be stored in your AWS R2 bucket, and will be available from your custom Imgix domain.

---

# Enterprise integration — Okta Single Sign-On

Source [docs]: https://www.datocms.com/marketplace/enterprise/okta-sso.md

### Automatically provision and (most importantly) deprovision DatoCMS users using your centralized Okta account

Automatic user provisioning is supported for the DatoCMS application.

This enables Okta to:

-   Add new users to DatoCMS
-   Update users’ profile information in DatoCMS
    
-   Deactivate users in DatoCMS
-   Push groups and memberships to DatoCMS
    

### Features

The following provisioning features are supported:

-   **Create User** - Creating a new user in Okta and assigning them to the DatoCMS application will create a new user in DatoCMS.
-   **Update User Attributes** - Updates to a user in Okta will be pushed to DatoCMS.
    
-   **Deactivate Users** - Deactivating the user or disabling the user's access to DatoCMS within OKTA will deactivate the user in DatoCMS.
-   **Reactivate Users** - User accounts can be reactivated from Okta.
    
-   **Import Users** - Users created in DatoCMS can be pulled into Okta and turned into new AppUser objects for matching against existing Okta users.
-   **Import Groups** - Groups created in DatoCMS can be pulled into Okta for reference within Okta.
    
-   **Push Groups** - Groups created in Okta can be pushed to DatoCMS. Attributes pushed include name and group members.
-   **Delete Groups** - Groups deleted or removed from the DatoCMS application within Okta will be deleted within DatoCMS.
    

### Prerequisites

-   Single Sign-On is only available for Enterprise plans.
    

### Configuration Steps

Switch your Okta dashboard to **Admin mode** by clicking the button in the upper right corner:

(Image content)

Then select **Applications** and click **Add Application**:

(Image content)

On the new page search for **DatoCMS** and press **Add**:

(Image content)

A new screen will appear. Give the new app a name and press **Next**:

(Image content)

Now log in to your DatoCMS project as an administrator, and navigate to **Settings \> Single Sign-On \> Settings**, and copy the value of the **SAML Token** field:

(Image content)

In Okta, scroll down to **Advanced Sign-On settings**, and paste the value taken from DatoCMS in the previous step inside the **Token** field:

(Image content)

Now copy the URL in the **Identity Provider metadata** field...

(Image content)

...and paste it into the DatoCMS **Identity Provider SAML Metadata URL** field:

(Image content)

Make sure to also specify the default role editors will be assigned (learn more about this field in the [Mapping Okta groups to DatoCMS roles](https://www.datocms.com/marketplace/enterprise/okta-sso.md#mapping-okta-groups-to-datocms-roles) chapter):

(Image content)

Press the **Save settings** button in DatoCMS. Back in Okta, select **Email** as the **Application username format** and press **Done**:

(Image content)

Now enter the **Provisioning** tab of your newly created DatoCMS application and click the **Configure API Integration** button:

(Image content)

Now in DatoCMS press the **Generate API Token** button under the **SCIM Settings** section:

(Image content)

Copy the newly generated token:

(Image content)

And paste the token inside the **API Token** field in Okta:

(Image content)

Click the **Test API Credentials** button and check that your credentials were verified successfully, then press **Save** to confirm.

Now in the **Provisioning \> To App** section, press the **Edit** button and:

-   Enable the **Create Users** option;
-   Enable the **Update User Attributes** option;
    
-   Enable the **Deactivate Users** option;
    

Press the **Save** button to confirm:

(Image content)

### Importing existing DatoCMS users in Okta

If you want to import existing users into Okta, enter the Provisioned users section in DatoCMS settings, and from there press the **Sync with regular users** button.

(Image content)

This will convert every DatoCMS collaborator into an SSO User:

(Image content)

Now under the DatoCMS app in Okta, find the **Import** tab, and click **Import Now**.

(Image content)

A list of DatoCMS users and possible associations with Okta users will be populated below. Click **Confirm Assignments** and these users will now be tracked, updated, and de-provisioned by Okta.

Now head over to the **Provisioning \> To App** section of Okta, and under **Attribute Mappings** press the **Force Sync** button:

(Image content)

If the integration is working correctly, you should see the imported users with the status **Synced**:

(Image content)

### Provisioning Okta users to DatoCMS

There are various ways to add new users to DatoCMS within Okta. The quickest way to assign multiple users at once is to navigate to the **Assignments** tab of the Application, and press the **Assign \> Assign to people button**:

(Image content)

From there, you will be able to assign users with the **Assign** button:

(Image content)

As soon as you add new users to the DatoCMS application, they will be visible in the **Provisioned users** section in DatoCMS.

### Managing provisioned user roles

Okta has the concept of [Groups](https://help.okta.com/en/prod/Content/Topics/users-groups-profiles/usgp-about-groups.htm). With Groups, Okta administrators can create different sets of users based on common themes, giving them different permissions.

You can leverage this feature to assign different DatoCMS roles to provisioned users.

#### Pushing groups

Create a group in Okta for each role available in your DatoCMS project. For example, if a "Blog Contributor" role exists in DatoCMS, create a "Blog Contributor" group in Okta.

Add members to the group in Okta.

(Image content)

Open the newly created group, and press the **Manage Apps button**. In the modal, assign the group to the DatoCMS application:

(Image content)

Open the DatoCMS Application in Okta, open the **Push Groups** tab and click on the **Push Groups \> Find groups by name** button:

(Image content)

Enter the first characters of the group name inside the text input, select the group from the dropdown and press **Save**:

(Image content)

If everything worked correctly, you should now see the same group under the **Groups** section in DatoCMS:

(Image content)

#### Mapping Okta groups to DatoCMS roles

In the **Groups** section in DatoCMS, you can now assign a specific role to each Group.

For each group, assign the role with the same name:

(Image content)

Once you've configured a role for every group, the following rules will apply:

-   The group's role will be applied to to every user belonging to it;
-   In case a user belongs to multiple groups, the first group in the list will be the one to win. You reorder groups with drag&drop to customize their priorities;
    

In case a user does not belong to any group, the default role specified in the **SSO Settings** will be used:

(Image content)

### Gotchas and Troubleshooting Tips

-   SAML Single Logout is currently not supported.
-   Users without **First Name** or/and **Last Name** in their DatoCMS profiles will be imported to Okta as "Unknown Unknown".
    
-   While it's technically possible to import DatoCMS Groups into Okta, it's not advisable to do so, as groups created in DatoCMS and imported into Okta cannot be deleted or changed in Okta. They must be managed in DatoCMS. It is suggested to create groups in Okta first and then push those groups to DatoCMS via the **Push Groups** button in Okta as described in the [Pushing groups](https://www.datocms.com/marketplace/enterprise/okta-sso.md#pushing-groups) chapter.
-   DatoCMS application supports Just-in-Time (JIT) provisioning. The SAML assertion will create an SSO user on the fly the first time they try to log in from the identity provider.
    
-   At the time of writing there's a known issue in Okta (Jira #OKTA-207372) that in some scenarios prevents Okta's administrators to completely remove users from groups. If a user belongs to just a single group, and you remove this user from the group, the user will be successfully deactivated, but it will still remain in the group. As soon as Okta solves this issues we'll update this documentation page.
    

For any other issues, please [contact our support](https://www.datocms.com/support.md) to get customized help.

---

# Enterprise integration — Microsoft Entra ID (formerly Azure AD)

Source [docs]: https://www.datocms.com/marketplace/enterprise/azure-active-directory.md

### Automatically provision and (most importantly) deprovision DatoCMS users using your centralized Microsoft Entra ID account

Automatic user provisioning is supported for the DatoCMS application.

This enables Microsoft Entra to:

-   Add new users to DatoCMS
-   Update users’ profile information in DatoCMS
    
-   Deactivate users in DatoCMS
-   Push groups and memberships to DatoCMS
    

### Features

The following provisioning features are supported:

-   **Create User** - Creating a new user in Microsoft Entra and assigning them to the DatoCMS application will create a new user in DatoCMS.
-   **Update User Attributes** - Updates to a user in Entra will be pushed to DatoCMS.
    
-   **Deactivate Users** - Deactivating the user or disabling the user's access to DatoCMS within Microsoft Entra will deactivate the user in DatoCMS.
-   **Reactivate Users** - User accounts can be reactivated from Microsoft Entra.
    
-   **Push Groups** - Groups created in Microsoft Entra can be pushed to DatoCMS. Attributes pushed include name and group members.
-   **Delete Groups** - Groups deleted or removed from the DatoCMS application within Microsoft Entra will be deleted within DatoCMS.
    

### Prerequisites

-   Single Sign-On is only available for Enterprise plans.
    

### Configuration Steps

Inside your Microsoft Azure dashboard search for **Microsoft Entra ID** and enter the service:

(Image content)

Enter the **Enterprise Applications** section, then click the **New Application** button:

(Image content)

Select **Create your own application**:

(Image content)

Name your application **DatoCMS** and click the **Create** button:

(Image content)

Enter the **Single Sign-On** section, then select **SAML** as single sign-on method:

(Image content)

Now click the small **Edit** button in the **Basic SAML Configuration** box, and fill in the following information:

-   **Identifier (Entity ID)**: `https://sso.datocms.com/<YOUR_SAML_TOKEN>/saml/metadata`
-   **Reply URL (Assertion Consumer Service URL)**: `https://sso.datocms.com/<YOUR_SAML_TOKEN>/saml/consume`
    
-   **Sign on URL (optional)**: `https://sso.datocms.com/<YOUR_PROJECT_ID>/saml/init`
    

Make sure to replace `<YOUR_SAML_TOKEN>` with the SAML Token present in the **Settings \> Single Sign-On \> Settings** section of your DatoCMS project:

(Image content)

Now move into the **Provisioning** section, and click the **Get started** button:

(Image content)

Within the **Settings \> Single Sign-On \> Settings** section of your DatoCMS project, click the **SCIM Settings \> API Token** button:

(Image content)

Copy the resulting API token:

(Image content)

Fill in the following information:

-   **Provisioning Mode**: Automatic
-   **Tenant URL**: https://sso.datocms.com/scim
    
-   **Secret Token**: use the API token we generated in the previous step
    

Then click the **Save** button:

(Image content)

Go back to the **Single Sign-On** section, copy the **App Federation Metadata Url**...

(Image content)

...and paste it into the DatoCMS **Identity Provider SAML Metadata URL** field:

(Image content)

Make sure to also specify the default role editors will be assigned to (learn more about this field in the "Mapping Microsoft Entra Groups to DatoCMS roles" section below):

(Image content)

Press the **Save settings** button in DatoCMS.

#### Mapping Microsoft Entra groups to DatoCMS roles

In the **Groups** section in DatoCMS, you can now assign a specific role to each Group. For each group, assign the role with the same name:

(Image content)

Once you've configured a role for every group, the following rules will apply:

-   The group's role will be applied to to every user belonging to it;
-   In case a user belongs to multiple groups, the first group in the list will be the one to win. You reorder groups with drag&drop to customize their priorities;
    

In case a user does not belong to any group, the default role specified in the **SSO Settings** will be used:

(Image content)

#### SAML User Attributes & Claims

DatoCMS recognizes the following claims for users (any other claim will be ignored):

(Image content)

#### Attribute Mapping

DatoCMS recognizes the following attributes for users (any other attribute will be ignored):

(Image content)

#### Support and Troubleshooting

For any issues, please [contact our support](https://www.datocms.com/support.md) to get customized help.

---

# Enterprise integration — OneLogin Single Sign-On

Source [docs]: https://www.datocms.com/marketplace/enterprise/onelogin-sso.md

### Automatically provision and (most importantly) deprovision DatoCMS users using your centralized OneLogin account

### Features

Automatic user provisioning is supported for the DatoCMS application.

This enables OneLogin to:

-   Add new users to DatoCMS
-   Update select fields in users’ profile information in DatoCMS
    
-   Deactivate users in DatoCMS
    

The following provisioning features are supported:

-   Push New Users
-   New users created through OneLogin will also be created in DatoCMS.
    
-   Push Profile Updates
-   Updates made to the user's profile through OneLogin will be pushed to DatoCMS.
    
-   Push User Deactivation
-   Deactivating the user or disabling the user's access to the application through OneLogin will deactivate the user in DatoCMS.
    
-   Import New Users
-   New users created in the third party application will be downloaded and turned into new AppUser objects, for matching against existing OneLogin users.
    

### Configuration Steps

Enter from your OneLogin dashboard the *Administration section* by clicking the button in the upper right corner:

(Image content)

Then select *Applications* and click *Add App*:

(Image content)

On the new page search for **DatoCMS**:

(Image content)

A new screen will appear. Give the new app a name and press *Save*:

(Image content)

Go into the **Configuration** page and under the *API Connection* section, fill in the following fields:

-   **DatoCMS SAML Token**: Copy the *SAML Token* field from DatoCMS and paste it here;
-   **SCIM Bearer Token**: Press the *Generate API Token* button under the *SCIM Settings* section in DatoCMS and paste it here;
    

(Video content)

When you're done, click the **Save** button, and then the **Enable** button. If everything works correctly, you should see the API Status marked as **Enabled**.

Now into the **SSO** page, copy the Issuer URL and paste it into the **Identity Provider Metadata URL** field in DatoCMS, and press the **Save settings** button:

(Video content)

In the *Provisioning* section:

-   Check the **Enable provisioning** option;
-   Uncheck the options to require admin approval befor performing operations (**Create user**, **Delete user**, **Update user**);
    

You can also change the default settings to control what action must be performed in DatoCMS when users are deleted or suspended in OneLogin.

When you're done, press the *Save* button to confirm:

(Image content)

### Import DatoCMS users in OneLogin

If you want to import existing users into OneLogin, enter the **Provisioned users** section in DatoCMS settings, and from there press the **Sync with regular users** button.

(Image content)

This will convert every DatoCMS collaborator into an SSO User:

(Image content)

You can now press the **Export CSV** button to download the CSV export file. Now go to the **Users** section in OneLogin, and press the **Import users** button:

(Image content)

A new panel will open up: press the **Upload File** button, and select the CSV file previously downloaded from DatoCMS. Press **Import** to start the process:

(Image content)

With OneLogin it's not possible to import memberships to an application, so you'll have add your existing users to the DatoCMS application manually.

### Provisioning OneLogin users to DatoCMS

OneLogin provides various ways to assign users to applications. For testing purposes we can assign a single user under **Users \> \[click on user name\] \> Applications tab**. Click the '+' sign to assign your testing user to the DatoCMS application.

(Image content)

Additional information about assigning users to applications in OneLogin can be found in [Assigning Apps to Users](https://onelogin.zendesk.com/hc/en-us/articles/202123134-Assigning-Apps-to-Users).

If the integration is working, you should now see the user present in DatoCMS under the **Provisioned users** section, with the status **Synced**:

(Image content)

### Managing DatoCMS roles within OneLogin

Groups created within OneLogin (at [https://subdomain.onelogin.com/groups](https://subdomain.onelogin.com/groups)) cannot be pushed to DatoCMS. Instead, in order for user membership to be managed via SCIM, groups must be created in DatoCMS and imported into OneLogin.

Enter the **Groups** section in DatoCMS settings, and from there press the **Sync with roles** button.

(Image content)

This will create an SSO Group for every role available in the project:

(Image content)

In the *Provisioning* section of your OneLogin application, press the **Refresh** button under the **Entitlements** section:

(Image content)

This will import DatoCMS Groups into OneLogin. Now go to the **Application \> Parameters** section in OneLogin, and click on the **Groups** table row:

(Image content)

A new modal will be opened. If the integration is working, you should see under the *Value* dropdown the groups we just created in DatoCMS:

(Image content)

Check the **Include in User Provisioning** option and hit *Save*:

(Image content)

### Assigning users to groups from OneLogin

Now that the setup is complete, you can proceed assigning users to groups. OneLogin provides various ways to do that.

For testing purposes we can assign a single user under **Applications \> Users \> \[click on user name\]**.

From there, you should be able to add one (or more) groups to the user:

(Image content)

If everything worked, you should now see the correct group associated to the user in DatoCMS:

(Image content)

You can also use OneLogin rules (mappings) to assign users to DatoCMS groups, IAM roles, and entitlements automatically, based on another OneLogin attribute, such as OneLogin Role.

Additional information about assigning groups to users in OneLogin can be found in [Mappings](https://onelogin.zendesk.com/hc/en-us/articles/201173404-Mappings).

---

# Enterprise integration — Google Workspace Single Sign-On

Source [docs]: https://www.datocms.com/marketplace/enterprise/google-workspace.md

### Automatically provision DatoCMS users using your centralized Google Workspace

### Prerequisites

-   Single Sign-On is only available for Enterprise plans.
    

### Configuration Steps

Enter your **Google Admin console** (at admin.google.com), go to [Apps \> Web and mobile apps](https://admin.google.com/ac/apps/unified) and click **Add App \> Add custom SAML app**.

(Image content)

Name your application **DatoCMS** and click the **Continue** button:

(Image content)

Download the IdP metadata by clicking on the **Download Metadata** button (we'll need this later), and click **Continue:**

(Image content)

Fill in some fields using the information present in the **Settings \> Single Sign-On \> Settings** section of your DatoCMS project:

-   **ACS URL:** Copy the value of the "Assertion Consumer Service (ACS) URL" field
-   **Entity ID:** Copy the value of the "Service Provider Metadata URL / Entity ID" field
    
-   **Name ID format:** EMAIL
    

You can leave the rest of the settings as they are, and then click **Continue**:

(Image content)

In the next section, copy the following mappings:

-   First name: `firstName`
-   Last name: `lastName`
    

(Image content)

If you want to also activate group mapping, then select the groups you want to pass in the SAML assertion, and specify `datocmsGroups` as **App attribute**:

(Image content)

Click **Finish**, then activate the App by clicking on **User Access**, and selecting **ON for everyone**:

(Image content)

Open the **IdP metadata file** that we previously downloaded on any text editor.

Now, return on the **Settings \> Single Sign-On \> Settings** section of your DatoCMS project, select **By passing the metadata XML**, and paste the complete content of the file in the **Identity Provider Metadata XML** field:

(Image content)

Make sure to also specify the default role collaborators will be assigned to (learn more about this field in the "Mapping groups to DatoCMS roles" section below):

(Image content)

Press the **Save settings** button in DatoCMS.

#### Mapping groups to DatoCMS roles

When a user logs in using SSO, the groups it belongs to will appear in the **Settings \> Single Sign-On \> Groups** section in DatoCMS.

In this section you can assign a specific DatoCMS role to each group:

(Image content)

Once configured, the following rules will apply:

-   The group's role will be applied to to every user belonging to it;
-   In case a user belongs to multiple groups, the first group in the list will be the one to win. You reorder groups with drag & drop to customize their priorities;
    

In case a user does not belong to any group, the default role specified in the **SSO Settings** will be used:

(Image content)

#### Support and Troubleshooting

For any issues, please [contact our support](https://www.datocms.com/support.md) to get customized help.

---

# Hosting & deployment — Netlify

Source [docs]: https://www.datocms.com/marketplace/hosting/netlify.md

### Trigger a build of your website on Netlify directly from the DatoCMS UI, and get a notification of the status of the build when it completes

Netlify is a very interesting service that combines a continuous deployment system with a powerful CDN optimized to host static websites. It's probably the easiest solution out there if you're exploring the world of static websites for the first time; furthermore, their free plan is perfectly compatible with DatoCMS and allows you to publish high-performant static websites.

**Warning:** this guide assumes you have a working static website project on your machine integrated with DatoCMS. If it's not the case, you can head over the [main documentation](/docs/general-concepts.md) to see how to properly configure the DatoCMS administrative area and how to integrate with your favorite static website generator.

### Step 1: create your Git repository

Create a new repository on [GitHub](https://github.com/new). To avoid errors, do not initialize the new repository with README, license, or gitignore files. You can add these files after your project has been pushed to GitHub.

Terminal window

```bash
$ git init
$ git add .
```

Commit the files that you've staged in your local repository.

Terminal window

```bash
$ git commit -m 'First commit'
```

At the top of your GitHub repository's Quick Setup page, click the clipboard icon to copy the remote repository URL. In Terminal, add the URL for the remote repository where your local repository will be pushed.

Terminal window

```bash
$ git remote add origin YOUR_GITHUB_REPOSITORY_URL
```

Now, it's time to push the changes in your local repository to GitHub.

Terminal window

```bash
git push -u origin master
```

Now that your project is up and running on GitHub, let's connect it to Netlify.

### Step 2: connect your repo to Netlify

Creating a new site on Netlify is simple. Once you've logged in, you'll be taken to [https://app.netlify.com/sites](https://app.netlify.com/sites). If you're just starting out, there's only one option:

(Image content)

Clicking "New Site" brings you to this screen:

(Image content)

Once you click on the "Link to GitHub" button, it will present the following screen:

(Image content)

Click the "Authorize Application" button to let Netlify read the list of your Github repositories. Like it says in the image above, Netlify doesn't store your GitHub access token on their servers. Once you've connected Netlify and GitHub, you can see a list of your Git repos. Select the one we just created:

(Image content)

The next screen is extremely important: it's where you instruct Netlify to build your static website:

(Image content)

Depending on your static generator the **Build command** and **Publish directory** field need to be filled with different values. In the *Site Settings*, make sure you add your DatoCMS read-only token as a `DATO_API_TOKEN` environment variable:

(Image content)

You can find your API token in the *Settings \> API tokens* section:

(Image content)

Once everything is ready, press the *Build your site* button. Netlify will run the build process for the first time and you can watch the progress of the operation.

(Image content)

Once the build process if finished, Netlify will publish under a temporary domain the directory specified earlier. Now everytime you push some change to GitHub, Netlify will repeat the build process and deploy a new version of the site.

(Image content)

### Step 3: connect Netlify to DatoCMS

There's only one last step needed: connecting DatoCMS to Netlify, so that everytime one of your editors press the *Publish changes* button in your administrative area, a new build process (thus a new publication of the final website) gets triggered.

(Video content)

Let's go through the process step-by-step. First, go to the *Settings \> Environments*, click on the plus icon and select *Netlify* as build method. The Netlify authorization window should pop up:

(Image content)

On the new window that pops up, click on "Grant Access" to allow DatoCMS to setup the auto-deploy meachanism and select the Netlify site that you want to link to DatoCMS, so that a number of bi-directional hooks willl be configured:

(Image content)

You can specify which branch of your Git repository you want to link and build with the deployment environment that you are creating:

(Image content)

When everything is done, confirm the integration pressing the **Save Settings** button.

---

# Hosting & deployment — Vercel

Source [docs]: https://www.datocms.com/marketplace/hosting/vercel.md

### Trigger a build of your website on Vercel directly from the DatoCMS UI, and get a notification of the status of the build when it completes

Vercel is a cloud platform for static sites and Serverless Functions that enables developers to host JAMstack websites and web services that deploy instantly, scale automatically. Their free plan is perfectly compatible with DatoCMS and allows you to publish high-performant static websites.

**Warning:** this guide assumes you have a working static website project on your machine integrated with DatoCMS. If it's not the case, you can return to the [previous sections](/docs/general-concepts.md) of this documentation to see how to properly configure the DatoCMS administrative area and how to integrate DatoCMS with your favorite static website generator.

If you are starting now with Vercel and DatoCMS we suggest trying one of our [starter projects](https://www.datocms.com/marketplace/starters.md). Dato will automatically create a brand-new project with all the necessary integrations!

### Step 1: Create a new project

Create a new repository on your favorite hosting service. To avoid errors, do not initialize the new repository with README, license, or `.gitignore` files. You can add these files after your project has been pushed to the hosting service.

Terminal window

```bash
$ git init
$ git add .
```

Commit the files that you've staged in your local repository.

Terminal window

```bash
$ git commit -m 'First commit'
```

At the top of your Git repository's Quick Setup page, click the clipboard icon to copy the remote repository URL. In Terminal, add the URL for the remote repository where your local repository will be pushed.

Terminal window

```bash
$ git remote add origin YOUR_GIT_REPOSITORY_URL
```

Now, it's time to push the changes in your local repository.

Terminal window

```bash
git push -u origin main
```

Now that your project is up and running on the Git hosting service, let's connect it to Vercel.

### Step 2: Create a new Vercel project from the Git repo

Now that you have your new repo, we can instruct Vercel to read from it and build our site.

To do that, follow Vercel instructions on [how to create a Vercel project](https://vercel.com/docs/concepts/git). Once you have done you should be able to see your new project on Vercel dashboard.

### Step 3: Connect Vercel to your DatoCMS project

By connecting DatoCMS to Vercel, every time your editors press the *Publish changes* button in your administrative area, a new build process on Vercel (thus a new publication of the final website) gets triggered.

To do that, start by clicking on the "Install this app" button on the top right. You will be redirected to your DatoCMS dashboard, where you will be asked to which project you want to add the integration.

(Image content)

Choose one, and you will be redirected to your project private area. Now you have to connect your project to Vercel.

(Video content)

This is all! Now you can control the build process directly from your DatoCMS private area!

---

# Hosting & deployment — Gitlab

Source [docs]: https://www.datocms.com/marketplace/hosting/gitlab.md

### Trigger a build of your website on Gitlab directly from the DatoCMS UI, and get a notification of the status of the build when it completes

**This guide assumes you have a working static website project on your machine integrated with DatoCMS**

If that's not your case, you can return to the previous sections of this documentation to see how to properly configure the DatoCMS administrative area and how to integrate DatoCMS with your favorite static website generator.

### Create your Git repository

DatoCMS supports both Gitlab.com and self-hosted instances of Gitlab CE. The first thing to do is to initialize a new Git repository on your website local directory:

Terminal window

```bash
$ git init
$ git add .
```

Commit the files that you've staged in your local repository.

Terminal window

```bash
$ git commit -m 'First commit'
```

Now create a new repository on [Gitlab](https://gitlab.com/projects/new). Once done, copy the remote repository URL. In Terminal, add the URL for the remote repository where your local repository will be pushed.

Terminal window

```bash
$ git remote add origin YOUR_GITLAB_REPOSITORY_URL
```

Now, it's time to push the changes in your local repository to Gitlab.

Terminal window

```bash
git push -u origin master
```

Now that your project is up and running on Gitlab, let's configure a Gitlab Pipeline that will publish your website on S3 after each further Git push.

### Enable Gitlab Pipeline

GitLab offers a continuous integration service out of the box. If you add a `.gitlab-ci.yml` file to the root directory of your repository, then each commit or push triggers your CI pipeline.

### Add the DatoCMS API token as environment variable

Reach the *Settings \> CI/CD Pipelines* settings page of your project, and in the *Variables* section, add an environment variable called `DATO_API_TOKEN` containing the read-only API token of your DatoCMS administrative area:

(Image content)

You can find the API token in the *Admin area \> API tokens* section:

(Image content)

### Configure .gitlab-ci.yml

The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs a pipeline with three stages: build, test, and deploy. You don't need to use all three stages; stages with no jobs are simply ignored.

Please refer to the official Gitlab documentation to learn everything regarding [how to configure your build](https://gitlab.com/help/ci/quick_start/README).

#### Jekyll

Here is an example `.gitlab-ci.yml` that you can use to run your build using Jekyll:

```yaml


# requiring the environment of Ruby 2.3.x
image: ruby:2.3

# add cache to 'vendor' for speeding up builds
cache:
  paths:
    - vendor/

before_script:
  - pip install awscli
  - bundle install --path vendor

variables:
  S3_BUCKET_NAME: "yourbucket"

# add a job called 'deploy'
deploy:
  script:
    # first dump all the remote content as local files
    - bundle exec dato dump
    # then generate the website
    - bundle exec dato jekyll build
    # copy the /public folder to S3 bucket
    - aws s3 cp ./ s3://$S3_BUCKET_NAME/ --recursive --exclude "*" --include "*.html"
  only:
    - master # the 'deploy' job will affect only the 'master' branch
```

---

# Hosting & deployment — Custom webhook

Source [docs]: https://www.datocms.com/marketplace/hosting/custom-webhook.md

### Trigger a build of your website directly from the DatoCMS UI, and get a notification of the status of the build when it completes

If our integrations with the most popular CI systems don't fit your use case, you can always fall back to custom webhooks.

With custom webhooks, every time an editor requires a new publication of the website with the *Publish changes* button, a POST request will be performed to a custom-endpoint you are in charge of specifying in the settings. The endpoint must respond with a 200 status code, and react producing a new publication of the website.

### Notifying DatoCMS about the result

Once you complete the publication process, you need to let DatoCMS how did it go, so we can in turn notify the editor. To do this, you need to make a POST request to a specific endpoint with a different JSON body depending on whether the publication was completed with success or it failed.

Terminal window

```bash


# Successful build notification example
curl -n -X POST https://webhooks.datocms.com/XXXXXXXXXXXXXXXXXXXX/deploy-results -H 'Content-Type: application/json' -d '{ "status": "success" }'

# Failed build notification example
curl -n -X POST https://webhooks.datocms.com/XXXXXXXXXXXXXXXXXXXX/deploy-results -H 'Content-Type: application/json' -d '{ "status": "error" }'
```

---

# DatoCMS Pricing

Source [marketing]: https://www.datocms.com/pricing.md

## Flexible pricing, ready to scale

Effortless maintenance, seamless operations: unlock substantial savings every year by leveraging DatoCMS headless technology and content infrastructure

#### Just getting started? Try DatoCMS out for free, forever (yes really)

Free plan comes with 2 editors and 300 records, with 10GB of traffic and 100k API calls each month. No overages allowed. [See all limits in detail](https://www.datocms.com/pricing.md#free-plan-details)

###### Free vs Professional

⚠️ **In the Free plan, you can't go over the allowed monthly limits.**  
If you reach these limits, the service will stop responding as expected.

| Plan limits & overages | Free plan | Professional plan |
| --- | --- | --- |
| Projects | 3 | 1 €39/mo per extra project |
| Sandbox environments | 3 | 3 €39/mo per extra sandbox environment |
| Collaborators | 1 | 10 €9/mo per extra collaborator |
| Models | 100 | 100 €10/mo every 10 extra models |
| Locales | 5 | 5 €19/mo per extra locale |
| Records | 300 | 100k €9/mo every 10k extra records |
| Bandwidth | 10GB/mo | 1TB/mo €29/mo every 150GB of extra traffic |
| CDA API Calls | 100k/mo | 1M/mo €9/mo every 1M extra CDA API calls |
| CMA API Calls | 25k/mo | 100k/mo €9/mo every 100k extra CMA API calls |
| Video streaming | 120 mins/mo | 50k mins/mo €9/mo every 12k mins of extra video streaming time |
| Support | Community-based | Mon-Fri, response in 24h |
| File storage | 200MB | 500GB |
| History retention | 3 days | 60 days |
| Site Search: Spiderable pages | 200 | 5k |

[**Working on many client projects?** Our Agency Partner Program starts at €39/month »](https://www.datocms.com/partner-program.md)

#### Professional

Everything you need — and more – to build professional digital projects

Start at €149 /month (billed annually) or **€199/month**

-   Generous quota included, with soft limits you can exceed and pay-as-you-go
-   10 collaborators included on each project (you can purchase more if needed)
-   Additional projects can be added for as low as €39/month
-   Expanded authoring roles to support most publishing workflows

#### Enterprise

Premium features, high-touch support and advanced compliance for scaled experiences

Custom payable by credit card or wire transfer

-   Guaranteed support and uptime SLAs
-   SSO, Audit logs and Static webhook IPs for enhanced security
-   Fully customizable roles and tasks for granular workflows, tailored to your specific needs
-   Support via shared Slack channel, editorial onboarding, plus access to our solution architects

### Compare plans

Explore our features and choose the best plan for you

| Features by plan | Professional From €149/month | Enterprise Tailored on your needs |
| --- | --- | --- |
| Projects | 1 included €39/mo per extra project, up to 10 | Custom |
| Sandbox environments | 3 included per project €39/mo per extra sandbox environment, up to 8 |
| Collaborators | 10 included per project €9/mo per extra collaborator, up to 100 |
| Models | 100 included per project €10/mo every 10 extra models, up to 200 |
| Locales | 5 included per project €19/mo per extra locale, up to 10 |
| Records | 100k included €9/mo every 10k extra records, up to 200k |
| Bandwidth | 1TB/mo included €29/mo every 150GB of extra traffic | Custom |
| CDA API Calls | 1M/mo included €9/mo every 1M extra CDA API calls |
| CMA API Calls | 100k/mo included €9/mo every 100k extra CMA API calls |
| Video streaming | 50k mins/mo included €9/mo every 12k mins of extra video streaming time |
| Support | Mon-Fri, response in 24h | Custom |
| File storage | 500GB |
| History retention | 60 days |
| Starter Projects | Yes | Yes |
| CLI tool | Yes | Yes |
| TypeScript API client | Yes | Yes |
| React, Vue, Svelte integration libraries | Yes | Yes |
| Responsive, progressive image components | Yes | Yes |
| Plugin SDK and UI system | Yes | Yes |
| GraphQL playground | Yes | Yes |
| Scripted content migrations | Yes | Yes |
| Automatic generation of migration scripts | Yes | Yes |
| Sandbox environments | Yes | Yes |
| Blocks | Yes | Yes |
| Custom API tokens with granular permissions | Yes | Yes |
| DatoCMS Site Search | Yes | Yes |
| Content Delivery API (GraphQL) | Yes | Yes |
| Content Preview Delivery API (GraphQL) | Yes | Yes |
| Real-time Updates API (GraphQL) | Yes | Yes |
| Content Management API (REST) | Yes | Yes |
| Images API | Yes | Yes |
| Video streaming API (adaptive bitrate) | Yes | Yes |
| Cache Tags | Yes | Yes |
| Delivery of content/assets via global CDN | Yes | Yes |
| Projects/User Management API (REST) | — | Yes |
| Powerful navigation and browsing of records | Yes | Yes |
| Structured rich-text editor | Yes | Yes |
| Markdown editor | Yes | Yes |
| Image editor | Yes | Yes |
| SEO/Social editor and preview | Yes | Yes |
| Landing page builder | Yes | Yes |
| Scheduled publishing | Yes | Yes |
| Content Validation | Yes | Yes |
| Locales/translations | Yes | Yes |
| Content Versioning & Rollbacks | Yes | Yes |
| Bulk editing | Yes | Yes |
| Links/reference fields | Yes | Yes |
| Single instance models | Yes | Yes |
| Tree-like models | Yes | Yes |
| Auto-publication of linked records on publish | Yes | Yes |
| Real-time preview of changes on website | Yes | Yes |
| Live collaboration | Yes | Yes |
| Advanced Media Area | Yes | Yes |
| AI-based smart image tagging | Yes | Yes |
| Visual Editing | Yes | Yes |
| Editorial Workflows | — | Yes |
| Locales | Yes | Yes |
| Localization granularity at per-field level | Yes | Yes |
| Optional/required locales | Yes | Yes |
| Selective per-locale publishing | Yes | Yes |
| 3rd-party services integration (Crowdin, Yandex, OpenAI, etc.) | Yes | Yes |
| Localized interface | Yes | Yes |
| Tailored UI Terminology | — | Yes |
| Per-locale roles & permissions | — | Yes |
| MCP server | Yes | Yes |
| LLM-ready docs | Yes | Yes |
| AI Content Translation | Yes | Yes |
| Community plugins (Marketplace) | Yes | Yes |
| Private plugins | Yes | Yes |
| Custom field editors | Yes | Yes |
| Custom sidebars | Yes | Yes |
| Custom pages | Yes | Yes |
| Custom record presentation | Yes | Yes |
| Integration with hosting (Netlify, Vercel, etc.) | Yes | Yes |
| Build triggers | Yes | Yes |
| Integration with DAMs (Bynder, Cloudinary, etc.) | Yes | Yes |
| Webhooks | Yes | Yes |
| Webhook custom transformation | Yes | Yes |
| Primary and sandbox environments | Yes | Yes |
| Fast environment fork | Yes | Yes |
| Primary environment hot swap | Yes | Yes |
| Automated environment migrations | Yes | Yes |
| Maintenance mode | Yes | Yes |
| Instant rollback | Yes | Yes |
| Image composition: User-defined focal point | Yes | Yes |
| Image manipulation | Yes | Yes |
| Image optimization | Yes | Yes |
| Image format conversion | Yes | Yes |
| Asset collections | Yes | Yes |
| Delivery via global CDN | Yes | Yes |
| Video transcoding | Yes | Yes |
| Adaptive bitrate streaming | Yes | Yes |
| Delivery via global CDN | Yes | Yes |
| Video streaming in 4K | — | Yes |
| Organizations | Yes | Yes |
| Organization roles | Yes | Yes |
| Project roles | Yes | Yes |
| Custom roles & permissions | Yes | Yes |
| Enforced Two-Factor Authentication | Yes | Yes |
| Custom editing domain | Yes | Yes |
| Single Sign-On (SSO) | — | Yes |
| SCIM provisioning via IdP | — | Yes |
| White-label experience | — | Yes |
| Audit logs | — | Yes |
| Custom assets domain | — | Yes |
| Custom assets storage (S3, GCP, etc.) | — | Yes |
| Static webhook IPs | — | Yes |
| Encryption in transit | Yes | Yes |
| Encryption at rest | Yes | Yes |
| ISO 27001 | Yes | Yes |
| Security reporting | — | Yes |
| Offline backups | — | Yes |
| 24/7 infrastructure monitoring | Yes | Yes |
| Content delivery network | Yes | Yes |
| Video delivery network | Yes | Yes |
| Image delivery network | Yes | Yes |
| Advanced CDN caching | Yes | Yes |
| Standard Terms and Services | Yes | Yes |
| Performance SLA | — | Yes |
| Support SLA | — | Yes |
| Dedicated shared Slack channel | — | Yes |
| Customer Success Manager | — | Yes |
| Editorial Onboarding | — | Yes |
| Infosec and legal review | — | Yes |
| Code Escrow Service | — | Yes |
| Community Forum support | Yes | Yes |
| Community Slack channel | Yes | Yes |
| Technical support | Yes | Yes |
| High-priority support | — | Yes |
| Payments with credit card | Yes | Yes |
| Payments with wire transfer | — | Yes |
| Invoices with Purchase Order # | Yes | Yes |

### Frequently Asked Questions

###### How do I pay?

We accept all major credit cards, including VISA, MasterCard, AMEX, Discover and more. We offer other custom billing solutions on Enterprise plans.

###### What occurs if I surpass the limits of my current plan?

The outcome depends on whether you are using a free or a paid plan. With the Free plan, it's not possible to exceed the permitted monthly limits. If you reach these limits, the service will cease to function as expected.

On the other hand, if you're subscribed to a paid plan, any additional usage beyond the plan's limits will be automatically charged. To keep track of your usage statistics, you can access your dashboard.

###### Can we run as many projects as we like?

Under the free plan, you can have up to a maximum of 3 projects. However, if you opt for the Professional plan, you'll get the capacity for 10 projects (inclusive of one project in the regular price and the option to add 9 extra projects for €39/month each). If you need more projects, just contact us via our [Support](https://www.datocms.com/support.md?topics=business-partnerships/general-requests) page.

###### Is there a required minimum contract duration?

Absolutely not! Whether you choose a monthly or annual plan, you'll make an upfront payment to use DatoCMS for the upcoming month or year, as per your selection. The best part is that you have the freedom to cancel at any point during your billing cycle without facing any charges. Plus, you'll receive a credit for the remaining unused time. This way, you have complete flexibility and control over your subscription.

###### Can I upgrade my plan mid-cycle?

Certainly! Upgrading your plan mid-cycle is possible. In such cases, the charge will be pro-rated, meaning you will only be billed for the cost of the new plan, taking into account the remaining unused amount from your current plan. This way, you'll be charged fairly for the upgraded features and time you use, making the process smooth and cost-effective.

###### How do I cancel my paying subscription?

To cancel your paying subscription, you have the option to switch to the free plan. However, please be aware that your current projects might exceed the limits allowed in the free plan. In order to proceed with the switch, you will need to take either of the following actions:

1.  Delete the project(s) that exceed the free plan limits.
    
2.  Reduce your project's resource usage to stay within the free plan limits.
    

By following these steps, you can successfully cancel your paying subscription and transition to the free plan.

###### How do monthly and annual pricing differ?

The monthly and annual pricing options have some differences. With the monthly plan, you are billed upfront for the first month, and then subsequently on the same date every month until you decide to switch to the free plan. In contrast, the yearly plan involves an upfront payment for the first year, followed by billing on the same date each year thereafter until you decide to switch to the free plan.

###### Do you offer discounts?

Yes, we do offer discounts! For teachers, students, and non-profits, we provide a generous 50% off on DatoCMS plan. To avail the discounted plan, simply get in touch with us. Additionally, if you're an agency, you can explore our [Partner Program](https://www.datocms.com/partner-program.md), which entitles you to a 30% discount on regular prices. Moreover, we offer credits for assisting with translations. To find more details, visit our [translations page](https://github.com/datocms/translations).

###### Is there a free trial?

The free plan provides access to all the functionalities of the Professional plan but with lower limits on resources. This should be sufficient for evaluating the product in most situations. However, if you still wish to try out the full Professional plan for any reason, you can [contact us](https://www.datocms.com/support.md?topics=free-trial), and we will be happy to activate a free trial for two weeks.

---

# DatoCMS Features

Source [marketing]: https://www.datocms.com/features.md

One CMS.  
Just enough features.

We believe in keeping things simple, and giving you the right feature-set tools to **get the job done**.

### Core Features

The essential features that you would interact with most often in DatoCMS.

### 📂 Projects

Easily manage various sites, apps, or clients using Projects.

Each Project has its own distinct content, settings, and branding. This setup helps your team stay organized and efficient, making it simpler to oversee different initiatives.

### 🌍 Environments

Sandbox environments allow developers to test changes without impacting production. Each sandbox is fully isolated, ensuring modifications don’t affect the primary project environment. This enables safe experimentation and development.

### 🏗️ Schema Builder

Our no-code schema builder let's you easily create models and blocks for your project. Define custom content types and fields with our flexible builder, making website or app customization simple and intuitive.

Each field type comes with several validations, configurations, and visibility options, ensuring you give your content team the right context and guardrails in place when working in the CMS.

### 1️⃣ Single Instance Models

Single Instance Models are perfect for pages you don’t plan to reuse, like your home page or “About Us” section. They ensure these unique pages stay one-of-a-kind, preventing any accidental duplicates and keeping your CMS organized.

### 🌳 Tree-like Models

Many content types require a structured hierarchy. Need categories and subcategories for your products or website navigation? Tree-like models are perfect for creating hierarchical, parent-child relationships, structuring your content efficiently for easier navigation and management.

### 🧱 Blocks

Define custom “Blocks” with specific fields like button text and links for a call-to-action or links and captions for an image carousel. Editors can reuse these blocks, while developers benefit from the consistency of strictly-defined fields.

Blocks can be nested within one another, added into models using "Modular Content", and embedded into Structured Text fields.

### 🗣️ Locales

Add locales to create and manage content in multiple languages within the same project. This feature simplifies the translation process and ensures your content is accessible to a global audience.

### 🎨 Media (Digital Assets)

Upload images, videos, documents, and audio files directly to your DatoCMS project and incorporate them into your content. These assets are automatically optimized and delivered to your users through our CDN, ensuring fast and efficient performance.

We work with imgix and Mux when handling images and videos to ensure optimized quality and performance with a wide range of parameters.

### 🌐 Global CDN

Ensure fast and reliable content delivery worldwide with our global Content Delivery Network (CDN). This reduces latency and improves user experience by distributing content closer to users.

### Editor Experience

Features specifically focused on giving content teams and creators the right tools.

### 🧩 Modular Content

Modular blocks allow you to define reusable custom components that enable your writers to build rich stories. Create engaging pages without developer help using our Modular Content field, with a highly intuitive WYSIWYG drag-and-drop experience.

### 📄 Structured Text

Our structured text editor is intuitive and powerful, so content editors write and format text, add images, links, and custom blocks in a snap. Familiar with WordPress, Notion, or Ghost? Your team will feel right at home.

### 🧙‍♂️ Visual Editing

Visual Editing lets content editors click directly on any element of your website and edit it in DatoCMS — no more hunting through record forms, switching tabs, or guessing which field maps to which headline.

Visual Editing supports two workflows.

**Click to Edit** is the simplest setup. Editors visit your website in draft mode, hover over content to see what's editable, and click to open DatoCMS in a new tab. It works entirely on your frontend without plugins.

**Visual Mode** builds up on the Web Previews plugin to give editors a side-by-side setup: preview on the left, edit panel on the right, click anything, edit immediately, see it update live. When they click on content, the edit panel opens instantly in the same view with no tab switching required.

### 🎞️ Media Area

Our Media Area streamlines the organization and management of your media assets. Easily retrieve and utilize images, videos, audio, and documents by filtering based on dominant colors, tags, EXIF data, size, orientation, notes, and more.

### ⏰ Scheduled Publishing

Plan and automate your content’s publication and unpublishing with scheduled publishing. This feature ensures timely updates and saves you the effort of manually pushing content live.

### 📝 Markdown Editor

For users who prefer Markdown, our dedicated editor supports seamless content creation using Markdown syntax, making the editorial process efficient and straightforward.

### 🤝 Live Collaboration

Work together with teammates in real-time. The multiplayer presence feature shows who’s active and contributing to the editorial process. And for safety, we'll lock records that someone's editing just so that you're not entering an endless loop of overrides 😅

### 🔍 Search and Filter Content

Utilize powerful filtering capabilities that allow you to save and share personalized views, making it easier to find and manage the content you need. Plus, search across all your content and find anything in a jiffy with DatoCMS Quick Search.

### 📊 Record Properties

Instantly view main record properties, including publishing status, linked records, and update history, for quick insights and efficient content management.

### 🧭 Easy Navigation

Use a powerful navigation system to tailor content organization to your editors’ needs. Group related models and blocks within the same section and add external links if necessary, ensuring a logical and efficient workflow.

### 🔗 Link Fields

Use links and reference fields to establish relationships between content items, fostering a structured content architecture and enabling effective cross-referencing.

### 📱 SEO and Social Editor

Customize SEO metadata and social media sharing settings for each piece of content. This helps improve your website’s discoverability and boosts user engagement.

### 🔗 Slugs and Permalinks

Use a special field type in your models to allow your editors to specify the URL permalink of a record, ensuring consistent and SEO-friendly URLs.

### 🌏 Locales and Translations

Effortlessly handle content in multiple languages with locale and translation support, enabling you to effectively reach a global audience.

### ✔️ Content Validation

Implement content validation rules to enforce consistency and quality. These rules ensure that content meets specified criteria before publication, maintaining high standards for your site.

### 📝 Editorial Workflows

Simplify content approval and publication with customizable editorial workflows, ensuring a smooth and efficient content lifecycle management.

### 📜 Content History & Versioning

Track and review content changes with a comprehensive history log for version control and auditing. Easily revert to previous versions to protect against accidental changes or unwanted updates, ensuring your content remains consistent and reliable.

### 📦 Bulk Actions

Easily update multiple records simultaneously with bulk actions, saving time and enhancing content management efficiency.

### 🔗 Auto-publish Linked Records

DatoCMS allows automatic and recursive publishing/unpublishing of linked records when the linking record is published/unpublished. This ensures consistent content across your site, saving time and effort for your editorial team.

### 👀 Real-time Previews

Experience real-time content changes on your live website directly within the CMS interface. This feature ensures an accurate representation of the final result before publishing, giving you confidence in your updates.

### 🔍 Media SEO

Set predefined title and alt meta tags at the asset level to boost SEO performance, create localized versions of these tags to amplify your content’s reach, or even define custom fields to use as metadata

### ✏️ Image Editor

Edit and optimize images directly within DatoCMS, saving time and ensuring visual consistency across your website or app.

### 🤖 AI Image Tagging

Simplify content organization with AI-powered smart image tagging. Automatically assign relevant tags like “portrait” or “nature” to images for easier search and categorization.

### 🌚 Dark Mode

DatoCMS respects your system settings out of the box by default. Got dark mode on in your OS? We're already dark when you log in. Change your system preference and we change too. Or set it manually if you prefer DatoCMS to stay in a specific way.

### Developer Experience

From our APIs to the CLI, we put a lot of focus on delivering a solid DX.

### 🚀 GraphQL Content Delivery API

Retrieve and display content effortlessly from DatoCMS using our Content Delivery API powered by GraphQL, ensuring smooth and efficient content delivery to end-users.

### 📡 Content Management API

Manage content programmatically with our REST Content Management API. Create, update, and remove any entity in your projects for efficient and automated content handling.

### 🎮 GraphQL Playground

Experiment with our GraphQL API interactively using the GraphQL playground, making API interaction and debugging simple and efficient.

### 👁️ Content Preview Delivery API

Preview and test content changes in real-time using our Content Preview Delivery API based on GraphQL. This feature enables efficient content editing and review, ensuring your updates are accurate before going live.

### ⚡ Real-time Updates API

Keep your staging or production websites synchronized with DatoCMS using real-time content changes through our GraphQL Real-time Updates API.

### 🏷️ Cache Tags

Implement cache tags to manage and precisely invalidate cached content, ensuring your website serves the most up-to-date content while optimizing performance, with no effort required from your development team.

### 📚 Integration libraries

Integrate DatoCMS with your preferred frontend frameworks using dedicated libraries for React/Next.js, Vue/Nuxt.js, and Svelte/SvelteKit.

### 🎯 Tech Starters

Accelerate your development process with pre-configured Starters. These templates provide a solid foundation for building websites using modern frameworks like Next.js, Astro, Remix, Svelte, Vue.js, and more, all integrated with DatoCMS.

### 🐢 MCP

Programmatically make changes to your content and schema directly from your AI tools like Claude Code and Cursor. The DatoCMS MCP uses a layered approach to provide 10 tools that run through discovery, planning, and execution, using scripts, documentation, and actual reasoning.

### 🤖 Agent Skills

Skills are markdown-based playbooks that your agent loads on demand. Each one covers a specific area of DatoCMS work with the kind of depth that lets your agent get things right on the first attempt: the right API shapes, the right patterns, the right conventions. Describe a task in plain language, the agent matches it to the right skill automatically, and it has everything it needs to get to work.

### 🤖 LLM Ready Docs

docs-full.txt is an LLM friendly way to utilize our complete documentation, all 500+ pages, available via one clean Markdown file optimized for LLM tools to consume and get accurate, context-aware answers about DatoCMS that's always up to date.

### 🛠️ Plugin SDK

Extend DatoCMS functionality to meet your specific needs with our Software Development Kit (SDK) and UI system, simplifying the process of creating custom plugins.

### 🔎 Site Search

Implement basic content search on your frontend with DatoCMS Site Search. This feature eliminates the need for costly third-party services like Algolia or Elastic Search, offering a cost-effective solution for your site’s search functionality.

### 🖼️ Images API

Use our Images API to handle and deliver images efficiently, optimizing website performance and ensuring a smooth user experience. The API supports image manipulation, optimization, and format conversions through URL parameters.

### 📹 Video Streaming API

Deliver high-quality video streaming with our adaptive bitrate Video API. It adjusts video quality based on users’ network conditions to ensure optimal viewing experiences.

### 🖼️ Image Components

Boost your website’s performance and UX with responsive, progressive images that adapt to screen sizes and network conditions using our imgix integration. Includes lazy loading and fast-loading placeholders (BlurHash and ThumbHash) by default.

### 🎥 Video Components

Integrate the pre-built video player component for your frontend framework of choice, for optimized video integration into your projects, enhancing your visitors' experience and simplifying development.

### 🔬 Deep Filtering

Filter Modular Content and Structured Text fields based on the content within their blocks, to handle complex queries with ease and access the data you need without unnecessary API calls.

### 👥 User Management API

Manage projects and user accounts using our Projects/User Management API based on REST, for efficient user administration and project management.

### 🎟️ Custom API tokens

Create API tokens with precise permissions to grant secure access to specific data and actions, enhancing the overall data security of your project.

### 💻 CLI

Streamline your workflow with our Command-Line Interface (CLI) tool to automate interactions with DatoCMS. Perform tasks directly from the command line, simplifying and speeding up your workflow.

### 📘 TypeScript API Client

Simplify API interactions and enhance code maintainability with our TypeScript API client. It provides type-safe access to DatoCMS APIs, improving developer productivity and ensuring robust code quality.

### 🤖 Autogenerated Migration Scripts

Automatically generate migration scripts when making schema changes, streamlining the migration process and reducing manual effort. This ensures smooth transitions and maintains data integrity.

### 📜 Scripted Content Migrations

Manage content updates and new releases with scripted content migrations, simplifying the process and ensuring data integrity throughout transitions.

### Image & Video Management

DatoCMS offers Digital Asset Management (DAM) out of the box to optimize your media.

### 🚀 Global CDN Delivery

Accelerate image and video delivery with a Content Delivery Network (CDN), reducing latency and buffering. This ensures faster load times and an optimal viewing experience, improving accessibility and user experience all over the world.

### 🔄 Image Transformations

Convert images to various formats, ensuring compatibility across different devices and platforms while maintaining visual fidelity with a simple URL param thanks to our deep integration with imgix.

### 📸 Image Editing

If you need to edit an uploaded image, you can use the built-in powerful editor to crop, rotate, apply predefined color filters, tweak colors, and add basic shapes and text to the image.

### 🖼️ Automatic Image Optimization

Improve your website’s performance and user experience by reducing image file sizes without compromising quality. This ensures faster loading times and a smoother browsing experience for your users, without any development effort.

### ✂️ Image Manipulation

It's no pro photo editing tool, but our inbuilt utilities let you easily edit and customize images to suit your needs for simple operations. Resize, crop, rotate, and apply filters to your images, giving you full control over their appearance. Add URL parameters when serving them to ensure they're being sent with the right transformations.

### 🎯 User-defined Focal Point

Set user-defined focal point for any image, which is maintained during resize or crop operations. This ensures responsive websites and various aspect ratios display the intended part of the image, such as a face or product.

### 🎬 Video Transcoding

Ensure smooth playback on any device by converting videos into multiple formats to suit various player capabilities. Upload videos in any supported format, and we’ll serve an optimized version to each viewer.

### 🍿 Video Editing

If you need to edit an uploaded video, you can use the built-in powerful editor to trim, resize, rotate, apply predefined color filters, tweak colors, and make basic changes to the video.

### 📊 Adaptive Bitrate Streaming

Deliver an uninterrupted viewing experience by dynamically adjusting video quality based on users’ network conditions. This ensures smooth playback for viewers with varying internet speeds, enhancing their overall experience.

### 🎧 Audio Tracks & Subtitles

Enhance your video’s accessibility and global reach by adding secondary audio tracks and subtitles. If you don't have subtitles, you can also auto-generate them using our speech recognition and machine learning technology thanks to our friends at Mux.

### 📺 4K Video Streaming

Let your viewers enjoy Ultra-HD streaming in 2K (1440p) or 4K (2160p). The video player will smartly pick the optimal resolution based on screen size and bandwidth, so that higher resolutions are streamed only when it makes sense.

### Localization

Granular localization options to ensure you connect with your customers wherever they are.

### 🗣️ Locales

Create separate locales for each language to manage content in multiple languages easily. This ensures a smooth and consistent multi-language experience for your global audience. Don't just stop at DE when you can meet your customers where they are in de-DE, de-AT, or de-CH.

### 🔤 Field-specific Localization

DatoCMS allows for localization options per field, rather than just the entire model, allowing you to choose which fields require translation and which can remain language-independent. This flexibility ensures accurate and relevant content for each locale.

### ✅ Required Locales

Decide whether specific locales are optional or required for content entry, allowing you to customize the localization process to fit your unique needs and ensure that no content is published without fulfilling the necessary criteria.

### 📤 Per-locale Publishing

Control the publishing (and unpublishing) of content on a per-locale basis, allowing you to release localized content according to your preferred schedule.

### 🔑 Per-locale Roles and Permissions

Assign language-specific roles and permissions to users, empowering them with the appropriate editing rights for managing content in different locales.

### 🔌 3rd-party Service Integrations

Integrate smoothly with popular localization services like Crowdin, Yandex and OpenAI to streamline your translation workflow and efficiently manage multilingual content.

### 🌐 Localized Interface

Enhance the user experience for content editors by offering a localized interface, allowing them to work in their preferred language (including French, Czech, German, and more) and timezone.

### 🎨 Personalized UI

Personalize the CMS dashboard by customizing UI labels to match your team’s terminology and preferences, creating a user-friendly environment tailored to your workflow.

### Extensibility

Plugins allow you to extend the capabilities of the CMS for specific use-cases.

### 👨‍👩‍👧‍👦 Community Plugins

Explore a wide variety of ready-to-use plugins developed by both, us, and the wider DatoCMS community, available in our Marketplace. These plugins extend functionality and speed up development for your projects.

### 🔒 Private Plugins

Create custom plugins tailored to your specific needs and keep them private. This ensures seamless integration with your DatoCMS instance while maintaining data security. DatoCMS supports plugins on the field level, for sidebars, and for entire pages.

### 🔧 Custom Field Plugins

Shopify product picker? AB test variation? Build and integrate unique field editors that perfectly match your content requirements, providing a tailored and intuitive content creation experience.

### 📌 Custom Sidebar Plugins

Tailor the sidebar of the editing interface with custom sections, providing your team with quick access to frequently used tools, widgets, and functionalities for a more efficient workflow.

### 📄 Custom Page Plugins

Craft personalized pages for your DatoCMS admin interface to provide a cohesive and branded experience for content editors, enhancing their workflow and familiarity with the platform.

### ☁️ Hosting Integrations

Integrate your DatoCMS project with hosting providers like Netlify and Vercel to speed up your development and deployment workflows, cutting the time needed to go-live with your project.

### 🔔 Build Triggers

Set up automated build triggers whenever content changes in DatoCMS. This ensures real-time content updates without the need for manual intervention with your deployment platforms like Vercel or Netlify.

### 🗄️ DAM Integrations

Got your own DAM? Integrate with popular Digital Asset Management systems like Cloudinary. This integrations simplify media management, making it easier to organize and utilize your existing and future digital assets.

### 🪝 Webhooks

Trigger custom actions with external services when specific events occur in DatoCMS, for advanced integration with your preferred tools and services, or just for simple oversight.

### 🔀 Webhook Custom Transformations

Customize outgoing webhook data to fit your desired format, ensuring smooth data handling and integration with your existing systems.

### Content Integrity

We have measures in place to ensure your content is not at risk of loss or inconsistencies.

### 🏝️ Primary and Sandbox Environments

The primary environment is used for regular editorial workflows. Sandbox environments let developers test and experiment with new content changes safely, acting like code branches for quick turnaround, without disrupting the editorial process.

### 🍴 Quick environment forks

Easily create sandbox environments by forking from existing ones. These exact copies include models, records, assets, plugins, locales, and more, allowing for smooth experimentation and development. You also have the option for "fast forks" with a more limited clone - for all those rapid changes.

### ⬆️ Environment Promotions

Instantly promote a sandbox environment to become the new primary environment without any service interruptions, ensuring smooth transitions and continuous availability.

### 🔄 Automated Environment Migrations

Use our CLI to automatically generate a migration script by comparing the differences between two environments, simplifying the update process.

### 🔧 Maintenance mode

Enable Maintenance Mode during migrations to ensure the integrity of your primary environment. This prevents changes during the migration process, reducing the risk of data divergence and maintaining content consistency. In fact, we recommend enabling it anyways for any schema and environment changes, you know, just to be safe.

### ⏪ Instant Rollback

A nifty fallback, DatoCMS comes with the ability to instantly rollback to a previous environment state. This feature allows you to undo any undesired changes, ensuring your content remains intact and reliable.

### Governance & Compliance

Robust features to put your mind at ease when using DatoCMS at scale.

### 🏛️ Organizations

Create distinct organizational units within your account to manage teams, projects, and resources separately. This enhances organization and control, allowing for better oversight and streamlined management.

### 🏢 Organization Roles

Assign roles to organization members, controlling access and privileges based on responsibilities. This ensures a structured workflow and maintains security by limiting access to sensitive information and critical functions.

### 🔐 Fine-grained Permissions

Define precise permissions for each role, detailing accessible content and actions for models, environments, locales, assets, and workflows. Use conditional rules to grant access based on real-time record statuses.

### 👥 Project Roles

Define role-based permissions within projects, customizing access rights for effective collaboration while maintaining data security. This ensures team members have appropriate access levels based on their roles and responsibilities.

### 👤 Custom Roles

Tailor access rights to fit your specific needs by creating custom roles for users and API tokens, each with permissions precisely set for their tasks. This ensures that team members have the exact access they need to perform their roles effectively and securely.

### 📈 Resource Usage Monitoring

Keep track of how your project is using resources like API calls, bandwidth, and video streaming, and get a bird's eye view of data usage across your projects from the Dashboard, so that you can troubleshoot issues and optimize costs.

### 🔢 2FA (Two-Factor Authentication)

Enhance your data security by enforcing two-factor authentication for all users, providing an additional layer of defense against unauthorized access.

### 🎫 Single Sign-On (SSO)

Simplify user authentication with SAML and Single Sign-On (SSO), enabling seamless access to your platform using existing credentials. This enhances both convenience and security for you and your users.

### ⚙️ SCIM Provisioning

Streamline user provisioning and management with SCIM (System for Cross-domain Identity Management) via your Identity Provider, ensuring a smooth user experience and high-grade security.

### 📋 Audit Logs

Gain valuable insights into user activities and platform events with comprehensive audit logs, enabling you to track changes and maintain compliance with ease.

### 🔗 Custom CMS Domain

Tailor your editing experience by customizing the domain through which users access DatoCMS, for a variety of brand and/or security reasons.

### 🌐 Custom Assets Domain

Ensure brand consistency by using a custom domain to host your assets, providing a cohesive experience for your users across all touchpoints, whether or not you use our in-built DAM.

### 📦 Custom Assets Storage

Already got an enterprise DAM or asset solution you're using? Keep them! We connect to your preferred cloud storage provider (such as S3 or Google Cloud Storage) to host your assets, meeting your specific performance and data storage requirements effectively.

### 🏷️ White-label Experience

Deliver a unified brand experience to your users or clients by white-labeling the platform, tailoring its appearance to mirror your brand’s unique identity and style.

### 📍 Static Webhook IPs

Uninterrupted connectivity with static webhook IPs offers a secure and reliable method for your servers to receive notifications about events occurring in your DatoCMS projects.

### Security & Infrastructure

Our foundations help companies of all sizes scale without obstacles.

### 🔒 Encryption in transit

Secure your data during transmission with encryption in transit, guaranteeing that all communication between your servers and ours, is safeguarded against unauthorized access, maintaining robust security protocols.

### 🔐 Encryption at rest

Rest easy knowing your data is secure even when stored on our servers, as encryption at rest ensures that all your information is protected and inaccessible to anyone without proper authorization.

### 🛡️ Security Reporting

Receive updates on potential threats or vulnerabilities, as well as on our proactive measures to mitigate them. Stay informed about the security of your account and data with regular security reporting.

### 💾 Offline Backups

Ensure data resilience and business continuity with offline backups, allowing you to recover critical information in the event of unforeseen data loss or system failures.

### 👁️ 24/7 Infrastructure Monitoring

Rest assured that our vigilant team is on top of your infrastructure's performance 24/7, promptly addressing any issues to maintain optimal functionality and minimize disruptions.

### ⚡ Advanced CDN Caching

Enhance your performance with advanced caching techniques that reduce load times by storing frequently accessed data. This results in faster page loads and an improved overall user experience.

### 🌍 Global Infrastructure

All our plans give you a global network of edge nodes to serve content, images, and videos from. With 75+ CDN edge nodes, we deliver over 500TB of your data every month with no hiccups.

---

# @datocms/cma-client — Content Management API JS/TS Client

Source [github]: https://raw.githubusercontent.com/datocms/js-rest-api-clients/main/packages/cma-client/README.md

Take a look at the full [API documentation](https://www.datocms.com/docs/content-management-api) for examples!

## Field Types

This library provides comprehensive TypeScript type definitions and utilities for all DatoCMS field types. Each field type includes type guards, validation functions, localization support, and editor appearance configurations.

### What's available

Every field type follows a consistent pattern providing:

- **Field value types**: TypeScript definitions for the field's data structure
- **Type guards**: Functions to validate field values at runtime
- **Localization support**: Utilities for handling localized field variants
- **Validation types**: Supported validators for the field type
- **Appearance configuration**: Editor types and their configuration options

**Example: `lat_lon` Field Type**

<details>
<summary>View example</summary>

```typescript
import { isLatLonFieldValue, isLocalizedLatLonFieldValue } from '@datocms/cma-client';
import type { LatLonFieldValue, LatLonFieldValidators, LatLonFieldAppearance } from '@datocms/cma-client';

// Field value type - object with latitude/longitude or null
const value: LatLonFieldValue = { latitude: 45.4642, longitude: 9.1900 };

// Type guard functions for validation
if (isLatLonFieldValue(someValue)) {
  // someValue is guaranteed to be { latitude: number; longitude: number } | null
}

if (isLocalizedLatLonFieldValue(localizedValue)) {
  // localizedValue is a localized lat/lon field
}

// Validator and appearance types available for type-safe configuration
type Validators = LatLonFieldValidators;
type Appearance = LatLonFieldAppearance;
```
</details>

### Context-Dependent field types

Some field types have different value formats depending on the API context (request vs response) or query parameters:

#### Request vs Response variations

**File and Gallery fields** have different type requirements for API requests versus responses:

<details>
<summary>View example</summary>

```typescript
import {
  FileFieldValue,
  FileFieldValueInRequest,
  GalleryFieldValue,
  GalleryFieldValueInRequest,
  // Type guards for runtime validation
  isFileFieldValue,
  isFileFieldValueInRequest,
  isGalleryFieldValue,
  isGalleryFieldValueInRequest
} from '@datocms/cma-client';

// API Response format - all metadata fields present with defaults
const fileResponse: FileFieldValue = {
  upload_id: "12345",
  alt: null,           // Always present (default: null)
  title: null,         // Always present (default: null)
  custom_data: {},     // Always present (default: {})
  focal_point: null    // Always present (default: null)
};

// API Request format - metadata fields are optional
const fileRequest: FileFieldValueInRequest = {
  upload_id: "12345"
  // alt, title, custom_data, focal_point are optional
};

// Runtime validation for different contexts
if (isFileFieldValueInRequest(someFileValue)) {
  // someFileValue has optional metadata fields
}

if (isGalleryFieldValue(someGalleryValue)) {
  // someGalleryValue is array of files with all metadata present
}
```
</details>

#### "Nested Mode" Response variations

**Block-containing fields** (`structured_text`, `single_block`, `rich_text`) support different block representations for regular responses, for ["Nested Mode" responses](https://www.datocms.com/docs/content-management-api/resources/item#api-response-modes-regular-vs-nested), and for requests:

<details>
<summary>View example</summary>

```typescript
import {
  StructuredTextFieldValue,
  StructuredTextFieldValueInRequest,
  StructuredTextFieldValueInNestedResponse,
  // Type guards for all variations (also available for single_block and rich_text)
  isStructuredTextFieldValue,
  isStructuredTextFieldValueInRequest,
  isStructuredTextFieldValueInNestedResponse
} from '@datocms/cma-client';

// Regular response - blocks as string IDs
const standard: StructuredTextFieldValue = {
  document: {
    type: "root",
    children: [
      {
        type: "block",
        // String ID reference
        item: "IdMLV2GJTXyQ0Bfns7R4IQ"
      }
    ]
  }
};

// Nested Mode response (?nested=true) - blocks as full objects
const nested: StructuredTextFieldValueInNestedResponse = {
  document: {
    type: "root",
    children: [
      {
        type: "block",
        // Always full block object
        item: {
          id: "IdMLV2GJTXyQ0Bfns7R4IQ",
          type: "item",
          attributes: { /* ... */ },
          relationships: { /* ... */ }
        }
      }
    ]
  }
};

// Request format - flexible block representation
const request: StructuredTextFieldValueInRequest = {
  document: {
    type: "root",
    children: [
      {
        type: "block",
        // Can be string ID, to keep block unchanged...
        item: "FicV5CxCSQ6yOrgfwRoiKA"
      },
      {
        type: "block",
        // ...or full block object (to create new blocks or update existing ones)
        item: {
          type: "item",
          attributes: { /* ... */ },
          relationships: { /* ... */ }
        }
      }
    ]
  }
};

// Runtime validation for different contexts
if (isStructuredTextFieldValueInNestedResponse(someStructuredText)) {
  // someStructuredText has blocks as full objects
}

if (isStructuredTextFieldValueInRequest(requestData)) {
  // requestData allows flexible block representations
}
```
</details>

These variants ensure type safety across different API contexts while maintaining the same conceptual data structure. All localized variants also have corresponding type guards (e.g., `isLocalizedStructuredTextFieldValueInRequest`, `isLocalizedStructuredTextFieldValueInNestedResponse`, etc.).

**TypeScript Generics Support:** For maximum type safety, all field value types and type guards for block-containing fields accept [`ItemTypeDefinition` generics](https://www.datocms.com/docs/content-management-api/resources/item#type-safe-development-with-typescript) to provide precise typing for your specific schema:

<details>
<summary>View example</summary>

```typescript
import type { MyArticle, MyArticleSection } from './schema';

// Fully typed structured text with specific block types
const content: StructuredTextFieldValueInRequest<MyArticleSection> = {
  document: {
    type: "root",
    children: [/* ... */]
  }
};

// Type guard with generic for precise validation
if (isStructuredTextFieldValueInNestedResponse<MyArticleSection>(value)) {
  // value is now typed with your specific block schema
}
```
</details>

## Block Processing Utilities

### Inspecting Records and Blocks

The `inspectItem()` function provides a visual, tree-structured representation of DatoCMS records in the console, making it easier to debug and understand complex content structures.

#### inspectItem()

Formats a DatoCMS item (record or block) as a visual tree structure, showing all fields with proper formatting for each field type. Particularly useful for debugging nested structures like modular content and structured text.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
function inspectItem(
  item: Item,
  options?: InspectItemOptions
): string

type InspectItemOptions = {
  maxWidth?: number; // Maximum width for text fields before truncation (default: 80)
}
```

**Parameters:**
- `item`: Any DatoCMS item, including records, blocks, or items in create/update format
- `options`: Optional configuration object
  - `maxWidth`: Maximum characters to display for text fields before truncating with "..."

**Returns:** A formatted string representation of the item as a tree structure

**Usage Example:**
```typescript
import { inspectItem } from '@datocms/cma-client';

const record = await client.items.find('MgCNaAI0RxSG8CA9sDXCHg');
console.log(inspectItem(record));

// Output:
// Item "MgCNaAI0RxSG8CA9sDXCHg" (item_type: "bJse85JFR0GbA37ey6kA1w")
// ├─ title: "My Blog Post"
// ├─ slug: "my-blog-post"
// └─ content:
//    ├─ en: "This is the English content..."
//    └─ it: "Questo è il contenuto italiano..."
```
</details>

### Creating and Duplicating Blocks

#### buildBlockRecord()

Converts a block data object into the proper format for API requests.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
function buildBlockRecord<D extends ItemTypeDefinition>(
  body: ItemUpdateSchema<ToItemDefinitionInRequest<D>>
): NewBlockInRequest<ToItemDefinitionInRequest<D>>
```

**Parameters:**
- `body`: Block data in update schema format

**Returns:** Formatted block record ready for API requests
</details>

#### duplicateBlockRecord()

Creates a deep copy of a block record, including all nested blocks, removing IDs to create new instances.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function duplicateBlockRecord<D extends ItemTypeDefinition>(
  existingBlock: ItemWithOptionalIdAndMeta<ToItemDefinitionInNestedResponse<D>>,
  schemaRepository: SchemaRepository
): Promise<NewBlockInRequest<ToItemDefinitionInRequest<D>>>
```

**Parameters:**
- `existingBlock`: The block to duplicate
- `schemaRepository`: Repository for schema lookups

**Returns:** New block record without IDs, ready to be created
</details>

### Narrowing Block Types

#### isBlockOfType()

Builds a type guard that narrows a union of block shapes to the one matching a given model. Meant for `Array#filter` / `Array#find` over block-bearing fields — either nested-response arrays (from `client.items.find(..., { nested: true })`) or request-payload arrays you're inspecting before sending.

TypeScript doesn't auto-narrow on discriminators buried in nested properties, so the natural-looking check `block.relationships.item_type.data.id === SOME_ID` won't narrow the block's type. This guard does the walk and returns a proper type-guard predicate.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
// Curried — returns a predicate (use with .filter / .find)
function isBlockOfType<Id extends string>(
  itemTypeId: Id,
): <T>(block: T) => block is NarrowBlockByItemType<T, Id>

// Direct — checks a single block inline (use inside `if`)
function isBlockOfType<T, Id extends string>(
  itemTypeId: Id,
  block: T,
): block is NarrowBlockByItemType<T, Id>

type NarrowBlockByItemType<T, Id extends string> = Extract<
  T,
  { relationships: { item_type: { data: { type: 'item_type'; id: Id } } } }
>
```

**Parameters:**
- `itemTypeId`: The item-type ID literal. For narrowing to work, the argument must be typed as a literal — use `as const` on pre-set ID constants. No `ItemTypeDefinition` type parameter is needed: `Extract` walks the input union using just the ID.
- `block` (direct form only): The block to check.

**Returns:**
- Curried form: a predicate `(block) => block is D-typed-block`.
- Direct form: a `boolean` that also acts as a type guard on `block`.

In both cases the guard:
- Narrows blocks carrying `relationships.item_type.data.id` — that covers `BlockInNestedResponse<D>` and the object variants of `BlockInRequest<D>` (`UpdatedBlockInRequest`, `NewBlockInRequest`).
- Returns `false` for plain string IDs (unchanged-reference form in request payloads) and for any non-block input.

The default (non-nested) response shape, where block fields are arrays of plain string IDs, is deliberately not supported — there's no way to recover the type from an ID alone.

**Usage Example:**
```typescript
import { isBlockOfType } from '@datocms/cma-client';

// ID of the ImageBlock model, one of several allowed inside Article `content`
const IMAGE_BLOCK_ID = 'FJM79jjKRMSVg-fR6k6X2A' as const;

const article = await client.items.find<Schema.Article>(articleId, { nested: true });

// Before: inline === check does not narrow
const images = article.content.filter(
  (b) => b.relationships.item_type.data.id === IMAGE_BLOCK_ID,
);
images[0].attributes.upload_id; // ❌ property does not exist on union

// After (curried): guard narrows the filter result
const images = article.content.filter(isBlockOfType(IMAGE_BLOCK_ID));
images[0].attributes.upload_id; // ✅ narrowed

// After (direct): inline narrowing on a single block
const first = article.content[0];
if (isBlockOfType(IMAGE_BLOCK_ID, first)) {
  first.attributes.upload_id; // ✅ narrowed
}
```

Use the curried form when you need a predicate for `.filter` / `.find`; use the direct form for one-off `if` checks. The `__itemTypeId` discriminator is also available for inline `switch` narrowing on a single value.
</details>

### Recursive Block Operations

DatoCMS supports three field types that can contain blocks: Modular Content (arrays of blocks), Single Block fields, and Structured Text (rich-text with embedded blocks). These functions abstract away the differences between field types and can traverse blocks recursively, processing nested blocks within blocks. They require a `SchemaRepository` instance to look up field definitions for nested blocks.

#### visitBlocksInNonLocalizedFieldValue()

Visit every block in a non-localized field value recursively, including blocks nested within other blocks.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function visitBlocksInNonLocalizedFieldValue(
  nonLocalizedFieldValue: unknown,
  fieldType: string,
  schemaRepository: SchemaRepository,
  visitor: (item: BlockInRequest, path: TreePath) => void | Promise<void>,
): Promise<void>
```

**Parameters:**
- `nonLocalizedFieldValue`: The non-localized field value
- `fieldType`: The type of DatoCMS field (ie. `string`, `rich_text`, etc.)
- `schemaRepository`: Repository for caching schema lookups
- `visitor`: Function called for each block (including nested)
</details>

#### mapBlocksInNonLocalizedFieldValue()

Transform all blocks in a non-localized field value recursively, including nested blocks.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function mapBlocksInNonLocalizedFieldValue(
  nonLocalizedFieldValue: unknown,
  fieldType: string,
  schemaRepository: SchemaRepository,
  mapper: (item: BlockInRequest, path: TreePath) => BlockInRequest | Promise<BlockInRequest>,
): Promise<unknown>
```

**Parameters:**
- `nonLocalizedFieldValue`: The non-localized field value
- `fieldType`: The type of DatoCMS field (ie. `string`, `rich_text`, etc.)
- `schemaRepository`: Repository for caching schema lookups
- `mapper`: Function that transforms each block

**Returns:** New field value
</details>

#### filterBlocksInNonLocalizedFieldValue()

Filter blocks recursively, removing blocks at any nesting level that don't match the predicate.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function filterBlocksInNonLocalizedFieldValue(
  nonLocalizedFieldValue: unknown,
  fieldType: string,
  schemaRepository: SchemaRepository,
  predicate: (item: BlockInRequest, path: TreePath) => boolean | Promise<boolean>,
): Promise<unknown>
```

**Parameters:**
- `nonLocalizedFieldValue`: The non-localized field value to filter
- `fieldType`: The type of DatoCMS field (ie. `string`, `rich_text`, etc.)
- `schemaRepository`: Repository for caching schema lookups
- `predicate`: Function that tests each block

**Returns:** New field value with filtered blocks

**Usage Example:**
```typescript
// Remove all video blocks at any nesting level
const noVideos = await filterBlocksInNonLocalizedFieldValue(
  schemaRepository,
  field,
  fieldValue,
  (block) => block.relationships.item_type.data.id !== 'video_block'
);
```
</details>

#### findAllBlocksInNonLocalizedFieldValue()

Find all blocks that match the predicate, searching recursively through nested blocks.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function findAllBlocksInNonLocalizedFieldValue(
  nonLocalizedFieldValue: unknown,
  fieldType: string,
  schemaRepository: SchemaRepository,
  predicate: (item: BlockInRequest, path: TreePath) => boolean | Promise<boolean>,
): Promise<Array<{ item: BlockInRequest; path: TreePath }>>
```

**Parameters:**
- `nonLocalizedFieldValue`: The non-localized field value to search
- `fieldType`: The type of DatoCMS field (ie. `string`, `rich_text`, etc.)
- `schemaRepository`: Repository for caching schema lookups
- `predicate`: Function that tests each block

**Returns:** Array of all matching blocks with their paths
</details>

#### reduceBlocksInNonLocalizedFieldValue()

Reduce all blocks recursively to a single value.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function reduceBlocksInNonLocalizedFieldValue<R>(
  nonLocalizedFieldValue: unknown,
  fieldType: string,
  schemaRepository: SchemaRepository,
  reducer: (accumulator: R, item: BlockInRequest, path: TreePath) => R | Promise<R>,
  initialValue: R,
): Promise<R>
```

**Parameters:**
- `nonLocalizedFieldValue`: The non-localized field value to reduce
- `fieldType`: The type of DatoCMS field (ie. `string`, `rich_text`, etc.)
- `schemaRepository`: Repository for caching schema lookups
- `reducer`: Function that processes each block
- `initialValue`: Initial accumulator value

**Returns:** The final accumulated value
</details>

#### someBlocksInNonLocalizedFieldValue()

Check if any block (including nested) matches the predicate.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function someBlocksInNonLocalizedFieldValue(
  nonLocalizedFieldValue: unknown,
  fieldType: string,
  schemaRepository: SchemaRepository,
  predicate: (item: BlockInRequest, path: TreePath) => boolean | Promise<boolean>,
): Promise<boolean>
```

**Parameters:**
- `nonLocalizedFieldValue`: The non-localized field value to test
- `fieldType`: The type of DatoCMS field (ie. `string`, `rich_text`, etc.)
- `schemaRepository`: Repository for caching schema lookups
- `predicate`: Function that tests each block

**Returns:** True if any block matches
</details>

#### everyBlockInNonLocalizedFieldValue()

Check if every block (including nested) matches the predicate.

<details>
<summary>View details</summary>

**TypeScript Signature:**
```typescript
async function everyBlockInNonLocalizedFieldValue(
  nonLocalizedFieldValue: unknown,
  fieldType: string,
  schemaRepository: SchemaRepository,
  predicate: (item: BlockInRequest, path: TreePath) => boolean | Promise<boolean>,
): Promise<boolean>
```

**Parameters:**
- `nonLocalizedFieldValue`: The non-localized field value to test
- `fieldType`: The type of DatoCMS field (ie. `string`, `rich_text`, etc.)
- `schemaRepository`: Repository for caching schema lookups
- `predicate`: Function that tests each block

**Returns:** True if all blocks match
</details>

## Unified Field Processing (Localized & Non-Localized)

These utilities provide a unified interface for working with DatoCMS field values that may or may not be localized. They eliminate the need for conditional logic when processing fields that could be either localized or non-localized.

#### mapNormalizedFieldValues() / mapNormalizedFieldValuesAsync()

Apply a transformation function to field values, handling both localized and non-localized fields uniformly.

<details>
<summary>View details</summary>

**TypeScript Signatures:**
```typescript
function mapNormalizedFieldValues<TInput, TOutput>(
  localizedOrNonLocalizedFieldValue: TInput | LocalizedFieldValue<TInput>,
  field: Field,
  mapFn: (locale: string | undefined, localeValue: TInput) => TOutput
): TOutput | LocalizedFieldValue<TOutput>

async function mapNormalizedFieldValuesAsync<TInput, TOutput>(
  localizedOrNonLocalizedFieldValue: TInput | LocalizedFieldValue<TInput>,
  field: Field,
  mapFn: (locale: string | undefined, localeValue: TInput) => Promise<TOutput>
): Promise<TOutput | LocalizedFieldValue<TOutput>>
```

**Parameters:**
- `localizedOrNonLocalizedFieldValue`: The field value (localized or non-localized)
- `field`: The DatoCMS field definition
- `mapFn`: Function to transform each value (receives locale for localized fields, undefined for non-localized)

**Returns:** Transformed value maintaining the same structure
</details>

#### filterNormalizedFieldValues() / filterNormalizedFieldValuesAsync()

Filter field values based on a predicate, handling both localized and non-localized fields.

<details>
<summary>View details</summary>

**TypeScript Signatures:**
```typescript
function filterNormalizedFieldValues<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  filterFn: (locale: string | undefined, localeValue: T) => boolean
): T | LocalizedFieldValue<T> | undefined

async function filterNormalizedFieldValuesAsync<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  filterFn: (locale: string | undefined, localeValue: T) => Promise<boolean>
): Promise<T | LocalizedFieldValue<T> | undefined>
```

**Parameters:**
- `localizedOrNonLocalizedFieldValue`: The field value to filter
- `field`: The DatoCMS field definition
- `filterFn`: Predicate function for filtering

**Returns:** Filtered value or undefined if all filtered out
</details>

#### visitNormalizedFieldValues() / visitNormalizedFieldValuesAsync()

Visit each value in a field, handling both localized and non-localized fields.

<details>
<summary>View details</summary>

**TypeScript Signatures:**
```typescript
function visitNormalizedFieldValues<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  visitFn: (locale: string | undefined, localeValue: T) => void
): void

async function visitNormalizedFieldValuesAsync<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  visitFn: (locale: string | undefined, localeValue: T) => Promise<void>
): Promise<void>
```

**Parameters:**
- `localizedOrNonLocalizedFieldValue`: The field value to visit
- `field`: The DatoCMS field definition
- `visitFn`: Function called for each value
</details>

#### someNormalizedFieldValues() / someNormalizedFieldValuesAsync()

Check if at least one field value passes the test.

<details>
<summary>View details</summary>

**TypeScript Signatures:**
```typescript
function someNormalizedFieldValues<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  testFn: (locale: string | undefined, localeValue: T) => boolean
): boolean

async function someNormalizedFieldValuesAsync<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  testFn: (locale: string | undefined, localeValue: T) => Promise<boolean>
): Promise<boolean>
```

**Parameters:**
- `localizedOrNonLocalizedFieldValue`: The field value to test
- `field`: The DatoCMS field definition
- `testFn`: Predicate function

**Returns:** True if any value passes the test
</details>

#### everyNormalizedFieldValue() / everyNormalizedFieldValueAsync()

Check if all field values pass the test.

<details>
<summary>View details</summary>

**TypeScript Signatures:**
```typescript
function everyNormalizedFieldValue<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  testFn: (locale: string | undefined, localeValue: T) => boolean
): boolean

async function everyNormalizedFieldValueAsync<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field,
  testFn: (locale: string | undefined, localeValue: T) => Promise<boolean>
): Promise<boolean>
```

**Parameters:**
- `localizedOrNonLocalizedFieldValue`: The field value to test
- `field`: The DatoCMS field definition
- `testFn`: Predicate function

**Returns:** True if all values pass the test
</details>

#### toNormalizedFieldValueEntries() / fromNormalizedFieldValueEntries()

Convert field values to/from a normalized entry format for uniform processing.

<details>
<summary>View details</summary>

**TypeScript Signatures:**
```typescript
function toNormalizedFieldValueEntries<T>(
  localizedOrNonLocalizedFieldValue: T | LocalizedFieldValue<T>,
  field: Field
): NormalizedFieldValueEntry<T>[]

function fromNormalizedFieldValueEntries<T>(
  entries: NormalizedFieldValueEntry<T>[],
  field: Field
): T | LocalizedFieldValue<T>

type NormalizedFieldValueEntry<T> = {
  locale: string | undefined;
  value: T;
}
```

**Parameters:**
- `localizedOrNonLocalizedFieldValue`/`entries`: Value to convert from/to
- `field`: The DatoCMS field definition

**Returns:** Normalized entries array or reconstructed field value

**Usage Example:**
```typescript
// Convert to entries for processing
const entries = toNormalizedFieldValueEntries(fieldValue, field);

// Process entries uniformly
const processed = entries.map(({ locale, value }) => ({
  locale,
  value: processValue(value)
}));

// Convert back to field value format
const result = fromNormalizedFieldValueEntries(processed, field);
```
</details>

## SchemaRepository

The `SchemaRepository` class provides a lightweight, in-memory cache for DatoCMS schema entities (item types, fields, fieldsets, and plugins). It helps avoid redundant API calls when working across multiple functions or utilities that require schema lookups.

**Why use it?**

- **Cache once, reuse everywhere**: The first API call stores results in memory; all subsequent lookups are instant.
- **Efficient schema access**: Retrieve entities by ID, API key, or package name without re-fetching.
- **Optimized for block processing**: Essential for utilities like `mapBlocksInNonLocalizedFieldValue`.
- **Fewer API calls**: Dramatically speeds up bulk operations and complex traversals.

**Usage Example:**

<details>
<summary>View example</summary>

```typescript
const schemaRepository = new SchemaRepository(client);

// First call: fetches from API and caches result
const blogPost = await schemaRepository.getItemTypeByApiKey('blog_post');
const fields = await schemaRepository.getItemTypeFields(blogPost);

// Next calls: resolved instantly from cache (no API calls)
const sameBlogPost = await schemaRepository.getItemTypeByApiKey('blog_post');
const sameFields = await schemaRepository.getItemTypeFields(blogPost);

// Works seamlessly with block-processing utilities
await mapBlocksInNonLocalizedFieldValue(
  fieldValue,
  fieldType,
  schemaRepository,  // share cached lookups
  async (block) => {
    // transform block here
  }
);
```
</details>

**When to Use**

* Traversing relationships that repeatedly query schema
* Bulk record processing scripts
* Block-processing utilities that need frequent lookups
* Any script where reducing API calls matters

**When Not to Use**

* Scripts that modify schema (models, fields, etc.)
* Long-running applications (cache never expires)
* Situations where the schema might change during execution

<details><summary><strong>Class signature</strong></summary>

```typescript
class SchemaRepository {
  constructor(client: GenericClient)

  // Item Type methods
  async getAllItemTypes(): Promise<ItemType[]>
  async getAllModels(): Promise<ItemType[]>
  async getAllBlockModels(): Promise<ItemType[]>
  async getItemTypeByApiKey(apiKey: string): Promise<ItemType>
  async getItemTypeById(id: string): Promise<ItemType>

  // Field methods
  async getItemTypeFields(itemType: ItemType): Promise<Field[]>
  async getItemTypeFieldsets(itemType: ItemType): Promise<Fieldset[]>

  // Higher-level utilities
  async getModelsEmbeddingBlocks(blocks: ItemType[]): Promise<ItemType[]>
  async getNestedBlocks(itemTypes: ItemType[]): Promise<ItemType[]>
  async getNestedModels(itemTypes: ItemType[]): Promise<ItemType[]>

  // Plugin methods
  async getAllPlugins(): Promise<Plugin[]>
  async getPluginById(id: string): Promise<Plugin>
  async getPluginByPackageName(packageName: string): Promise<Plugin>

  // Raw variants (return API response format)
  async getAllRawItemTypes(): Promise<RawItemType[]>
  async getRawItemTypeByApiKey(apiKey: string): Promise<RawItemType>
  async getRawNestedBlocks(itemTypes: Array<ItemType | RawItemType>): Promise<Array<RawItemType>>
  async getRawNestedModels(itemTypes: Array<ItemType | RawItemType>): Promise<Array<RawItemType>>
  // ... and more raw variants
}
```
</details>

## Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/datocms/js-rest-api-clients. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.

## License

The package is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).

---

# @datocms/cda-client — Content Delivery API JS/TS Client

Source [github]: https://raw.githubusercontent.com/datocms/cda-client/main/README.md

A lightweight, TypeScript-ready package that offers various helpers around the native Fetch API to perform GraphQL requests towards DatoCMS [Content Delivery API](https://www.datocms.com/docs/content-delivery-api).

## TypeScript Support

This package is built with TypeScript and provides type definitions out of the box. It supports `TypedDocumentNode` for improved type inference when using [gql.tada](https://gql-tada.0no.co/), [GraphQL Code Generator](https://the-guild.dev/graphql/codegen) or similar tools.

## Examples

### Basic Query Execution

```typescript
import { executeQuery } from "@datocms/cda-client";

const query = `
  query {
    allArticles {
      id
      title
    }
  }
`;

const result = await executeQuery(query, {
  token: "your-api-token-here",
});

console.log(result);
```

### Using with TypeScript and GraphQL Code Generator

```typescript
import { executeQuery } from "@datocms/cda-client";
import { AllArticlesQuery } from "./generated/graphql";

const result = await executeQuery(AllArticlesQuery, {
  token: "your-api-token-here",
  variables: {
    limit: 10,
  },
});

console.log(result.allArticles);
```

## Installation

```bash
npm install @datocms/cda-client
```

## Usage

This package provides several utility functions to help you interact with the DatoCMS Content Delivery API using GraphQL.

### `executeQuery`

The main function to execute a GraphQL query against the DatoCMS Content Delivery API.

```typescript
import { executeQuery } from "@datocms/cda-client";

const result = await executeQuery(query, options);
```

#### Parameters

- `query`: A GraphQL query string, `DocumentNode`, or `TypedDocumentNode`.
- `options`: An object containing execution options.

#### Options

| Option               | Type                   | Description                                                                                                                                                   |
| -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `token`              | `string`               | DatoCMS API token (required) [Read more](https://www.datocms.com/docs/content-delivery-api/authentication)                                                    |
| `includeDrafts`      | `boolean`              | If true, return draft versions of records [Read more](https://www.datocms.com/docs/content-delivery-api/api-endpoints#preview-mode-to-retrieve-draft-content) |
| `excludeInvalid`     | `boolean`              | If true, filter out invalid records [Read more](https://www.datocms.com/docs/content-delivery-api/api-endpoints#strict-mode-for-non-nullable-graphql-types)   |
| `environment`        | `string`               | Name of the DatoCMS environment for the query [Read more](https://www.datocms.com/docs/content-delivery-api/api-endpoints#specifying-an-environment)          |
| `contentLink`        | `'vercel-v1'`          | If true, embed metadata for Content Link [Read more](https://www.datocms.com/docs/content-delivery-api/api-endpoints#content-link)                            |
| `baseEditingUrl`     | `string`               | Base URL of your DatoCMS project [Read more](https://www.datocms.com/docs/content-delivery-api/api-endpoints#content-link)                                    |
| `returnCacheTags`    | `boolean`              | If true, receive Cache Tags associated with the query [Read more](https://www.datocms.com/docs/content-delivery-api/api-endpoints#cache-tags)                 |
| `variables`          | `object`               | Variables to be sent with the query                                                                                                                           |
| `fetchFn`            | `function`             | Custom fetch function (optional)                                                                                                                              |
| `requestInitOptions` | `Partial<RequestInit>` | Additional request initialization options (optional)                                                                                                          |
| `autoRetry`          | `boolean`              | Automatically retry on rate limit (default: true)                                                                                                             |

### `rawExecuteQuery`

Similar to `executeQuery`, but returns both the query result and the full response object. This can be handy when used together with returnCacheTags to actually retrieve the cache tags.

```typescript
import { rawExecuteQuery } from "@datocms/cda-client";

const [result, response] = await rawExecuteQuery(query, {
  token: "your-api-token-here",
  returnCacheTags: true,
});
const cacheTags = response.headers.get("x-cache-tags");
```

### `executeQueryWithAutoPagination`

This function comes handy when the query contains a paginated collection: behind the scene,
`executeQueryWithAutoPagination` reworks the passed query and collects the results, so that
it's possible to get a collection of records that is longer than Content Delivery API's result limit.
That is done with a single API call, in a transparent way.

```typescript
import { executeQueryWithAutoPagination } from "@datocms/cda-client";

const result = await executeQueryWithAutoPagination(query, options);
```

#### Parameters

Parameters are the same available for `executeQuery`:

- `query`: A GraphQL query string, `DocumentNode`, or `TypedDocumentNode`.
- `options`: An object containing execution options with the same shape of options for `executeQuery`.

### How does it work?

Suppose you want to execute the following query on an model with `2500` records:

```graphql
query BuildSitemapUrls {
  allBlogPosts {
    slug
  }

  entries: allSuccessStories(first: 2500) {
    ...SuccessStoryUrlFragment
  }
}

fragment SuccessStoryUrlFragment on SuccessStoryRecord {
  slug
}
```

Well, that's a roadblock: The CDA is limited to returning a maximum of `500` items at a time. If you try to fetch more than that, you'll get an error. Instead, if you wanted to fetch all `2500` records, you would normally have to manually paginate it by executing the query multiple times, each time incrementing the `skip` parameter by an additional 500. That's a lot of work!

Fortunately, the helper function `executeQueryWithAutoPagination` does that on your behalf: the above query is analyzed and rewritten on the fly like this:

```graphql
query BuildSitemapUrls {
  allBlogPosts {
    slug
  }
  splitted_0_entries: allSuccessStories(first: 500, skip: 0) {
    ...SuccessStoryUrlFragment
  }
  splitted_500_entries: allSuccessStories(first: 500, skip: 500) {
    ...SuccessStoryUrlFragment
  }
  splitted_1000_entries: allSuccessStories(first: 500, skip: 1000) {
    ...SuccessStoryUrlFragment
  }
  splitted_1500_entries: allSuccessStories(first: 500, skip: 1500) {
    ...SuccessStoryUrlFragment
  }
  splitted_2000_entries: allSuccessStories(first: 500, skip: 2000) {
    ...SuccessStoryUrlFragment
  }
}

fragment SuccessStoryUrlFragment on SuccessStoryRecord {
  slug
}
```

Once executed, the results get collected and recomposed as if nothing happened.

#### Limitations

`executeQueryWithAutoPagination` works only when the query contains only one selection that has 
an oversized `first:` argument (i.e. the `first:` argument surpasses the Content Delivery API's result limit of `500`).
If two or more requested models have oversized pagination, the function will return an error.

The rewritten query must still respect the [GraphQL complexity cost](https://www.datocms.com/docs/content-delivery-api/complexity).

### `rawExecuteQueryWithAutoPagination`

As for `executeQuery`, also `executeQueryWithAutoPagination` has a pair raw version that returns both the query result and the full response object.
This can be handy when used together with returnCacheTags to actually retrieve the cache tags.

```typescript
import { rawExecuteQueryWithAutoPagination } from "@datocms/cda-client";

const [result, response] = await rawExecuteQueryWithAutoPagination(query, {
  token: "your-api-token-here",
  returnCacheTags: true,
});
const cacheTags = response.headers.get("x-cache-tags");
```

### `buildRequestHeaders`

Builds request headers for a GraphQL query towards the DatoCMS Content Delivery API.

```typescript
import { buildRequestHeaders } from "@datocms/cda-client";

const headers = buildRequestHeaders(options);
```

#### Options

The `buildRequestHeaders` function accepts the same options as `executeQuery`, except for `variables`, `fetchFn`, and `autoRetry`.

### `buildRequestInit`

Builds the request initialization object for a GraphQL query towards the DatoCMS Content Delivery API.

```typescript
import { buildRequestInit } from "@datocms/cda-client";

const requestInit = buildRequestInit(query, options);
```

#### Parameters

- `query`: A GraphQL query string or `DocumentNode`.
- `options`: An object containing execution options (same as `executeQuery`).

## Error Handling

In case a query fails (either with an HTTP status code outside of the 2xx range, or for an error in the query), an `ApiError` exception will be thrown by the client. This error contains all the details of the request and response, allowing you to debug and handle errors effectively.

### Example

```typescript
import { executeQuery, ApiError } from "@datocms/cda-client";

const query = `
  query {
    allArticles {
      id
      title
    }
  }
`;

try {
  const result = await executeQuery(query, {
    token: "your-api-token-here",
  });
  console.log(result);
} catch (e) {
  if (e instanceof ApiError) {
    // Information about the failed request. The API token is redacted from
    // `e.options`: an error tends to end up in logs and error trackers, which
    // are no place for a token.
    console.log(e.query);
    console.log(e.options);

    // Information about the response
    console.log(e.response.status);
    console.log(e.response.statusText);
    console.log(e.response.headers);
    console.log(e.response.body);
  } else {
    // Handle other types of errors
    throw e;
  }
}
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Trying a change before it's released

Every push to a branch here publishes a preview of the package, which you can
install anywhere — no npm release, no `npm link`:

```
npm i https://pkg.pr.new/@datocms/cda-client@<commit-sha>
```

The exact URL shows up in the commit's check run on GitHub, and in a comment on
the pull request once there is one. This is the supported way to try a change
inside a site that lives in another repository, or to hand a fix to whoever
reported it before it is released.

Previews are throwaway: they are never published to npm, and the URL stops
resolving after a while. Never commit one to a `package.json` that ships.

## Releasing (maintainers)

Every user-visible change needs a changeset: run `npx changeset` from the repo
root in the same PR, pick the bump level (`patch` is for bug fixes only, new API
surface is `minor`) and commit the file it writes under `.changeset/`. That is
where the changelog entry comes from, and it is where the bump level is decided
— not on release day. See [`.changeset/README.md`](.changeset/README.md).

To release, from an up-to-date, clean `main`, run `npm run release`. It
builds and tests, applies the pending changesets — bumping the version and
writing `CHANGELOG.md` — publishes to npm, and only then tags `vX.Y.Z`, pushes,
and creates a GitHub release whose notes come straight from that changelog
entry. An interrupted release is resumed by re-running it, never undone. Use
`npm run release:next` for a prerelease under the `next` dist-tag.

The script is
[`@datocms/release-toolchain`](https://github.com/datocms/release-toolchain),
shared with every other DatoCMS repository and pinned here by tag.

## License

This project is licensed under the MIT License.

---

# DatoCMS CLI — Command-line tool for migrations, schema, and CMA

Source [github]: https://raw.githubusercontent.com/datocms/cli/main/packages/cli/README.md

DatoCMS CLI tool for managing DatoCMS projects, environments and schemas.

<!-- toc -->
* [DatoCMS CLI](#datocms-cli)
* [Usage](#usage)
* [Commands](#commands)
<!-- tocstop -->

<br /><br />
<a href="https://www.datocms.com/">
<img src="https://www.datocms.com/images/full_logo.svg" height="60">
</a>
<br /><br />

# Usage

```sh-session
$ npm install -g datocms

$ datocms COMMAND
running command...

$ datocms (--version)
datocms/0.1.6 darwin-x64 node-v16.20.0

$ datocms --help [COMMAND]
USAGE
  $ datocms COMMAND
...
```

# Commands

<!-- commands -->
* [`datocms autocomplete [SHELL]`](#datocms-autocomplete-shell)
* [`datocms cma:call RESOURCE METHOD`](#datocms-cmacall-resource-method)
* [`datocms cma:docs [RESOURCE] [ACTION]`](#datocms-cmadocs-resource-action)
* [`datocms cma:script [FILE]`](#datocms-cmascript-file)
* [`datocms environments:destroy ENVIRONMENT_ID`](#datocms-environmentsdestroy-environment_id)
* [`datocms environments:fork SOURCE_ENVIRONMENT_ID NEW_ENVIRONMENT_ID`](#datocms-environmentsfork-source_environment_id-new_environment_id)
* [`datocms environments:list`](#datocms-environmentslist)
* [`datocms environments:primary`](#datocms-environmentsprimary)
* [`datocms environments:promote ENVIRONMENT_ID`](#datocms-environmentspromote-environment_id)
* [`datocms environments:rename ENVIRONMENT_ID NEW_ENVIRONMENT_ID`](#datocms-environmentsrename-environment_id-new_environment_id)
* [`datocms help [COMMAND]`](#datocms-help-command)
* [`datocms link`](#datocms-link)
* [`datocms login`](#datocms-login)
* [`datocms logout`](#datocms-logout)
* [`datocms maintenance:off`](#datocms-maintenanceoff)
* [`datocms maintenance:on`](#datocms-maintenanceon)
* [`datocms migrations:new NAME`](#datocms-migrationsnew-name)
* [`datocms migrations:run`](#datocms-migrationsrun)
* [`datocms plugins`](#datocms-plugins)
* [`datocms plugins:add PLUGIN`](#datocms-pluginsadd-plugin)
* [`datocms plugins:available`](#datocms-pluginsavailable)
* [`datocms plugins:inspect PLUGIN...`](#datocms-pluginsinspect-plugin)
* [`datocms plugins:install PLUGIN`](#datocms-pluginsinstall-plugin)
* [`datocms plugins:link PATH`](#datocms-pluginslink-path)
* [`datocms plugins:remove [PLUGIN]`](#datocms-pluginsremove-plugin)
* [`datocms plugins:reset`](#datocms-pluginsreset)
* [`datocms plugins:uninstall [PLUGIN]`](#datocms-pluginsuninstall-plugin)
* [`datocms plugins:unlink [PLUGIN]`](#datocms-pluginsunlink-plugin)
* [`datocms plugins:update`](#datocms-pluginsupdate)
* [`datocms projects:list [QUERY]`](#datocms-projectslist-query)
* [`datocms schema:generate FILENAME`](#datocms-schemagenerate-filename)
* [`datocms schema:inspect [FILTER]`](#datocms-schemainspect-filter)
* [`datocms unlink`](#datocms-unlink)
* [`datocms whoami`](#datocms-whoami)

## `datocms autocomplete [SHELL]`

Display autocomplete installation instructions.

```
USAGE
  $ datocms autocomplete [SHELL] [-r]

ARGUMENTS
  [SHELL]  (zsh|bash|powershell) Shell type

FLAGS
  -r, --refresh-cache  Refresh cache (ignores displaying instructions)

DESCRIPTION
  Display autocomplete installation instructions.

EXAMPLES
  $ datocms autocomplete

  $ datocms autocomplete bash

  $ datocms autocomplete zsh

  $ datocms autocomplete powershell

  $ datocms autocomplete --refresh-cache
```

_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v3.2.56/src/commands/autocomplete/index.ts)_

## `datocms cma:call RESOURCE METHOD`

Call any DatoCMS Content Management API method

```
USAGE
  $ datocms cma:call RESOURCE... METHOD... [--json] [--config-file <value>] [--profile <value>] [--api-token
    <value>] [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [-e <value>] [--data
    <value>] [--params <value>]

ARGUMENTS
  RESOURCE...  The resource to call (e.g., items, itemTypes, etc.)
  METHOD...    The method to execute (e.g., list, find, create, etc.)

FLAGS
  -e, --environment=<value>  Environment to execute the command in
      --data=<value>         JSON/JSON5 string containing the request body data (for create/update operations)
      --params=<value>       JSON/JSON5 string containing query parameters

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Call any DatoCMS Content Management API method

EXAMPLES
  List all roles

    $ datocms cma:call roles list

  Find a specific role

    $ datocms cma:call roles find 123

  Create a new role

    $ datocms cma:call roles create --data '{name: "Editor", can_edit_site: true}'

  Update a role

    $ datocms cma:call roles update 123 --data '{name: "Updated Name"}'

  Delete a role

    $ datocms cma:call roles destroy 123

  List items with query parameters

    $ datocms cma:call items list --params '{filter: {type: "blog_post"}}'

  Execute command in a specific environment

    $ datocms cma:call items list --environment my-environment
```

_See code: [src/commands/cma/call.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/cma/call.ts)_

## `datocms cma:docs [RESOURCE] [ACTION]`

Browse the DatoCMS Content Management API reference documentation

```
USAGE
  $ datocms cma:docs [RESOURCE] [ACTION] [--expand-details <value>...] [--expand-types <value>...]
    [--types-depth <value>]

ARGUMENTS
  [RESOURCE]  The resource to describe (e.g., items, uploads)
  [ACTION]    The action to describe (e.g., create, instances)

FLAGS
  --expand-details=<value>...  Expand a collapsed <details> section by its summary text (repeatable). Pass `*` to expand
                               every collapsed section
  --expand-types=<value>...    Inline TypeScript definitions for types referenced by the action, suppressing all other
                               output. Pass `*` to expand every reachable type, or specific type names (repeatable) to
                               expand just those
  --types-depth=<value>        Maximum depth when walking referenced types (default: 2). Has no effect with
                               `--expand-types "*"`, which disables the depth limit

DESCRIPTION
  Browse the DatoCMS Content Management API reference documentation

EXAMPLES
  List all available resources

    $ datocms cma:docs

  Describe a specific resource and its actions

    $ datocms cma:docs items

  Describe a specific action with examples

    $ datocms cma:docs items create

  Expand a collapsed details section

    $ datocms cma:docs items create --expand-details "Example: Basic example"

  Inline definitions for every reachable referenced type

    $ datocms cma:docs items create --expand-types "*"

  Inline only specific referenced types

    $ datocms cma:docs items create --expand-types ItemCreateSchema
```

_See code: [src/commands/cma/docs.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/cma/docs.ts)_

## `datocms cma:script [FILE]`

Run a one-off TypeScript script against the Content Management API.

```
USAGE
  $ datocms cma:script [FILE] [--json] [--config-file <value>] [--profile <value>] [--api-token <value>]
    [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [-e <value>] [-f <value>]
    [--timeout <value>] [--rebuild-workspace] [--skip-validation]

ARGUMENTS
  [FILE]  Path to a TypeScript file to run (file-mode). Alternative to --file. If omitted and --file is not set, the
          script is read from stdin (stdin-mode).

FLAGS
  -e, --environment=<value>  Environment to execute the script against
  -f, --file=<value>         Path to a TypeScript file to run (file-mode). If omitted, the script is read from stdin
                             (stdin-mode).
      --rebuild-workspace    Wipe and rebuild the internal workspace used by stdin-mode (node_modules, tsconfig), then
                             exit without running any script. Use after a CLI upgrade if stdin scripts fail with module
                             resolution errors.
      --skip-validation      Skip source validation and (stdin-mode only) TypeScript type-checking before execution
      --timeout=<value>      Kill the script if it runs longer than this many seconds. Default: no timeout.

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Run a one-off TypeScript script against the Content Management API.

  Two modes of invocation, different ergonomics:

  File-mode  — Pass a .ts file path. The script must export a default
  async function `(client: Client) => Promise<void>`.
  It is loaded from its original location (via tsx), which
  means imports resolve against your project's node_modules
  and your editor LSP gives you full type feedback. No
  typecheck is performed before execution — same behavior as
  `migrations:run`. Use it for scripts that are long enough
  that a shell heredoc becomes awkward, use local helper
  modules, or need to be rerunnable by filename.

  Stdin-mode — Pipe plain top-level-await code via stdin. `client` (a
  pre-authenticated CMA client) and, on-demand, `Schema`
  (project-specific ItemTypeDefinition types) are available
  as ambient globals. `export default` is not supported here.
  Ideal for throwaway one-liners and pipes.

  These are *both* for one-off, throwaway work. If you need to commit and
  replay a script across environments, use `migrations:new` /
  `migrations:run` instead.

  Source validation (both modes):
  - Explicit `any` / `unknown` types are rejected. Use specific types.
  - Casts to `never` (e.g. `x as never`, `<never>x`) are rejected.
  - `@ts-ignore`, `@ts-expect-error`, and `@ts-nocheck` directives are
  rejected — fix the underlying type error instead.
  - File-mode: script must have a default export; top-level is rejected.
  - Stdin-mode: script must be top-level; default export is rejected.

  Stdin-mode — pre-installed packages (importable only here):
  - @datocms/cma-client-node
  - datocms-structured-text-utils
  - datocms-structured-text-dastdown
  In file-mode you have your own `node_modules` — install whatever you
  need there.

  Stdin-mode — ambient globals (no import needed):
  - `client` (pre-authenticated CMA client)
  - `Schema.*` (project-specific ItemTypeDefinition types, on demand)
  - All named exports of `@datocms/cma-client-node`,
  `datocms-structured-text-utils`, and
  `datocms-structured-text-dastdown` are exposed as globals — e.g.
  `buildBlockRecord(...)`, `mapNodes(...)`, `parse(...)`,
  `ApiTypes.Item`, `SchemaRepository`.
  Use `console.log()` for output. stdout is piped through cleanly so the
  command composes with `| jq` and similar.

EXAMPLES
  File-mode — run a script from a file

    $ datocms cma:script ./my-script.ts

  Same as above, using the --file flag

    $ datocms cma:script --file ./my-script.ts

  File-mode — typical script shape (requires `datocms` installed in the script's project)

    $ datocms cma:script <<'EOF' > ./my-script.ts && datocms cma:script ./my-script.ts \
      import type { Client } from 'datocms/lib/cma-client-node'; \
      export default async function(client: Client) { \
      const itemTypes = await client.itemTypes.list(); \
      console.log(itemTypes.map((t) => t.api_key)); \
      } \
      EOF

  Stdin-mode — one-liner via pipe

    echo 'console.log((await client.itemTypes.list()).map(t => t.api_key))' | datocms cma:script

  Stdin-mode — type-safe record creation using the ambient Schema

    $ datocms cma:script <<'EOF' \
      await client.items.create<Schema.Article>({ \
      item_type: { id: 'ABC123', type: 'item_type' }, \
      title: 'Hello world', \
      }); \
      EOF

  Stdin-mode — pipe output into jq

    echo 'console.log(JSON.stringify(await client.itemTypes.list()))' | datocms cma:script 2>/dev/null | jq \
      '.[].api_key'
```

_See code: [src/commands/cma/script.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/cma/script.ts)_

## `datocms environments:destroy ENVIRONMENT_ID`

Destroys a sandbox environment

```
USAGE
  $ datocms environments:destroy ENVIRONMENT_ID [--json] [--config-file <value>] [--profile <value>] [--api-token <value>]
    [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory]

ARGUMENTS
  ENVIRONMENT_ID  The environment to destroy

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Destroys a sandbox environment
```

_See code: [src/commands/environments/destroy.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/environments/destroy.ts)_

## `datocms environments:fork SOURCE_ENVIRONMENT_ID NEW_ENVIRONMENT_ID`

Creates a new sandbox environment by forking an existing one

```
USAGE
  $ datocms environments:fork SOURCE_ENVIRONMENT_ID NEW_ENVIRONMENT_ID [--json] [--config-file <value>] [--profile
    <value>] [--api-token <value>] [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory]
    [--force --fast]

ARGUMENTS
  SOURCE_ENVIRONMENT_ID  The environment to copy
  NEW_ENVIRONMENT_ID     The name of the new sandbox environment to generate

FLAGS
  --fast   Run a fast fork. A fast fork reduces processing time, but it also prevents writing to the source environment
           during the process
  --force  Forces the start of a fast fork, even there are users currently editing records in the environment to copy

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Creates a new sandbox environment by forking an existing one
```

_See code: [src/commands/environments/fork.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/environments/fork.ts)_

## `datocms environments:list`

Lists primary/sandbox environments of a project

```
USAGE
  $ datocms environments:list [--json] [--config-file <value>] [--profile <value>] [--api-token <value>] [--log-level
    NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory]

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Lists primary/sandbox environments of a project
```

_See code: [src/commands/environments/list.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/environments/list.ts)_

## `datocms environments:primary`

Returns the name the primary environment of a project

```
USAGE
  $ datocms environments:primary [--json] [--config-file <value>] [--profile <value>] [--api-token <value>] [--log-level
    NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory]

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Returns the name the primary environment of a project
```

_See code: [src/commands/environments/primary.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/environments/primary.ts)_

## `datocms environments:promote ENVIRONMENT_ID`

Promotes a sandbox environment to primary

```
USAGE
  $ datocms environments:promote ENVIRONMENT_ID [--json] [--config-file <value>] [--profile <value>] [--api-token <value>]
    [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory]

ARGUMENTS
  ENVIRONMENT_ID  The environment to promote

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Promotes a sandbox environment to primary
```

_See code: [src/commands/environments/promote.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/environments/promote.ts)_

## `datocms environments:rename ENVIRONMENT_ID NEW_ENVIRONMENT_ID`

Renames an environment

```
USAGE
  $ datocms environments:rename ENVIRONMENT_ID NEW_ENVIRONMENT_ID [--json] [--config-file <value>] [--profile <value>]
    [--api-token <value>] [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory]

ARGUMENTS
  ENVIRONMENT_ID      The environment to rename
  NEW_ENVIRONMENT_ID  The new environment ID

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Renames an environment
```

_See code: [src/commands/environments/rename.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/environments/rename.ts)_

## `datocms help [COMMAND]`

Display help for datocms.

```
USAGE
  $ datocms help [COMMAND...] [-n]

ARGUMENTS
  [COMMAND...]  Command to show help for.

FLAGS
  -n, --nested-commands  Include all nested commands in the output.

DESCRIPTION
  Display help for datocms.
```

_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/6.2.45/src/commands/help.ts)_

## `datocms link`

Link the current directory to a DatoCMS project and configure it

```
USAGE
  $ datocms link [--json] [--config-file <value>] [--profile <value>] [--log-level
    NONE|BASIC|BODY|BODY_AND_HEADERS] [--migrations-dir <value>] [--migrations-model <value>] [--migrations-template
    <value>] [--migrations-tsconfig <value>] [--organization-id <value>] [--site-id <value>]

FLAGS
  --log-level=<option>           Level of logging to use for the profile
                                 <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --migrations-dir=<value>       Directory where script migrations will be stored
  --migrations-model=<value>     API key of the DatoCMS model used to store migration data
  --migrations-template=<value>  Path of the file to use as migration script template
  --migrations-tsconfig=<value>  Path of the tsconfig.json to use to run TS migration scripts
  --organization-id=<value>      Organization ID to use
  --profile=<value>              [default: default] Name of the profile to create/update
  --site-id=<value>              Site ID to link to

GLOBAL FLAGS
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.

DESCRIPTION
  Link the current directory to a DatoCMS project and configure it
```

_See code: [src/commands/link.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/link.ts)_

## `datocms login`

Authenticate with DatoCMS via OAuth

```
USAGE
  $ datocms login [--json]

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Authenticate with DatoCMS via OAuth

EXAMPLES
  $ datocms login
```

_See code: [src/commands/login.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/login.ts)_

## `datocms logout`

Log out of DatoCMS by removing stored credentials

```
USAGE
  $ datocms logout [--json]

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Log out of DatoCMS by removing stored credentials

EXAMPLES
  $ datocms logout
```

_See code: [src/commands/logout.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/logout.ts)_

## `datocms maintenance:off`

Take a project out of maintenance mode

```
USAGE
  $ datocms maintenance:off [--json] [--config-file <value>] [--profile <value>] [--api-token <value>] [--log-level
    NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory]

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Take a project out of maintenance mode
```

_See code: [src/commands/maintenance/off.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/maintenance/off.ts)_

## `datocms maintenance:on`

Put a project in maintenance mode

```
USAGE
  $ datocms maintenance:on [--json] [--config-file <value>] [--profile <value>] [--api-token <value>] [--log-level
    NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [--force]

FLAGS
  --force  Forces the activation of maintenance mode even there are users currently editing records

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Put a project in maintenance mode
```

_See code: [src/commands/maintenance/on.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/maintenance/on.ts)_

## `datocms migrations:new NAME`

Create a new migration script

```
USAGE
  $ datocms migrations:new NAME [--json] [--config-file <value>] [--profile <value>] [--api-token <value>]
    [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [--ts | --js] [--template <value>
    | --autogenerate <value>] [--schema <value>]

ARGUMENTS
  NAME  The name to give to the script

FLAGS
  --autogenerate=<value>
      Auto-generates script by diffing the schema of two environments

      Examples:
      * --autogenerate=foo finds changes made to sandbox environment 'foo' and applies them to primary environment
      * --autogenerate=foo:bar finds changes made to environment 'foo' and applies them to environment 'bar'

  --js
      Forces the creation of a JavaScript migration file

  --schema=<value>
      Include schema definitions for models and blocks (TypeScript only). Use "all" for all item types, or specify
      comma-separated API keys for specific ones

  --template=<value>
      Start the migration script from a custom template

  --ts
      Forces the creation of a TypeScript migration file

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Create a new migration script
```

_See code: [src/commands/migrations/new.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/migrations/new.ts)_

## `datocms migrations:run`

Run migration scripts that have not run yet

```
USAGE
  $ datocms migrations:run [--json] [--config-file <value>] [--profile <value>] [--api-token <value>] [--log-level
    NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [--source <value>] [--allow-primary ]
    [--dry-run] [--force [--fast-fork [--destination <value> | --in-place]]] [--migrations-dir <value>]
    [--migrations-model <value>] [--migrations-tsconfig <value>]

FLAGS
  --allow-primary                Allow running migrations in-place on the primary environment. Only use for strictly
                                 additive migrations (no data/schema destruction): there is no rollback if the run fails
                                 partway through
  --destination=<value>          Specify the name of the new forked environment
  --dry-run                      Simulate the execution of the migrations, without making any actual change
  --fast-fork                    Run a fast fork. A fast fork reduces processing time, but it also prevents writing to
                                 the source environment during the process
  --force                        Forces the start of a fast fork, even there are users currently editing records in the
                                 environment to copy
  --in-place                     Run the migrations in the --source environment, without forking
  --migrations-dir=<value>       Directory where script migrations are stored
  --migrations-model=<value>     API key of the DatoCMS model used to store migration data
  --migrations-tsconfig=<value>  Path of the tsconfig.json to use to run TS migrations scripts
  --source=<value>               Specify the environment to fork

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Run migration scripts that have not run yet
```

_See code: [src/commands/migrations/run.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/migrations/run.ts)_

## `datocms plugins`

List installed plugins.

```
USAGE
  $ datocms plugins [--json] [--core]

FLAGS
  --core  Show core plugins.

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  List installed plugins.

EXAMPLES
  $ datocms plugins
```

_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.5.1/src/commands/plugins/index.ts)_

## `datocms plugins:add PLUGIN`

Installs a plugin into datocms.

```
USAGE
  $ datocms plugins:add PLUGIN... [--json] [-f] [-h] [-s | -v]

ARGUMENTS
  PLUGIN...  Plugin to install.

FLAGS
  -f, --force    Force npm to fetch remote resources even if a local copy exists on disk.
  -h, --help     Show CLI help.
  -s, --silent   Silences npm output.
  -v, --verbose  Show verbose npm output.

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Installs a plugin into datocms.

  Uses npm to install plugins.

  Installation of a user-installed plugin will override a core plugin.

  Use the DATOCMS_NPM_LOG_LEVEL environment variable to set the npm loglevel.
  Use the DATOCMS_NPM_REGISTRY environment variable to set the npm registry.

ALIASES
  $ datocms plugins:add

EXAMPLES
  Install a plugin from npm registry.

    $ datocms plugins:add myplugin

  Install a plugin from a github url.

    $ datocms plugins:add https://github.com/someuser/someplugin

  Install a plugin from a github slug.

    $ datocms plugins:add someuser/someplugin
```

## `datocms plugins:available`

Lists official DatoCMS CLI plugins

```
USAGE
  $ datocms plugins:available [--json]

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Lists official DatoCMS CLI plugins
```

_See code: [src/commands/plugins/available.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/plugins/available.ts)_

## `datocms plugins:inspect PLUGIN...`

Displays installation properties of a plugin.

```
USAGE
  $ datocms plugins:inspect PLUGIN...

ARGUMENTS
  PLUGIN...  [default: .] Plugin to inspect.

FLAGS
  -h, --help     Show CLI help.
  -v, --verbose

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Displays installation properties of a plugin.

EXAMPLES
  $ datocms plugins:inspect myplugin
```

_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.5.1/src/commands/plugins/inspect.ts)_

## `datocms plugins:install PLUGIN`

Installs a plugin into datocms.

```
USAGE
  $ datocms plugins:install PLUGIN... [--json] [-f] [-h] [-s | -v]

ARGUMENTS
  PLUGIN...  Plugin to install.

FLAGS
  -f, --force    Force npm to fetch remote resources even if a local copy exists on disk.
  -h, --help     Show CLI help.
  -s, --silent   Silences npm output.
  -v, --verbose  Show verbose npm output.

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Installs a plugin into datocms.

  Uses npm to install plugins.

  Installation of a user-installed plugin will override a core plugin.

  Use the DATOCMS_NPM_LOG_LEVEL environment variable to set the npm loglevel.
  Use the DATOCMS_NPM_REGISTRY environment variable to set the npm registry.

ALIASES
  $ datocms plugins:add

EXAMPLES
  Install a plugin from npm registry.

    $ datocms plugins:install myplugin

  Install a plugin from a github url.

    $ datocms plugins:install https://github.com/someuser/someplugin

  Install a plugin from a github slug.

    $ datocms plugins:install someuser/someplugin
```

_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.5.1/src/commands/plugins/install.ts)_

## `datocms plugins:link PATH`

Links a plugin into the CLI for development.

```
USAGE
  $ datocms plugins:link PATH [-h] [--install] [-v]

ARGUMENTS
  PATH  [default: .] path to plugin

FLAGS
  -h, --help          Show CLI help.
  -v, --verbose
      --[no-]install  Install dependencies after linking the plugin.

DESCRIPTION
  Links a plugin into the CLI for development.

  Installation of a linked plugin will override a user-installed or core plugin.

  e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello'
  command will override the user-installed or core plugin implementation. This is useful for development work.


EXAMPLES
  $ datocms plugins:link myplugin
```

_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.5.1/src/commands/plugins/link.ts)_

## `datocms plugins:remove [PLUGIN]`

Removes a plugin from the CLI.

```
USAGE
  $ datocms plugins:remove [PLUGIN...] [-h] [-v]

ARGUMENTS
  [PLUGIN...]  plugin to uninstall

FLAGS
  -h, --help     Show CLI help.
  -v, --verbose

DESCRIPTION
  Removes a plugin from the CLI.

ALIASES
  $ datocms plugins:unlink
  $ datocms plugins:remove

EXAMPLES
  $ datocms plugins:remove myplugin
```

## `datocms plugins:reset`

Remove all user-installed and linked plugins.

```
USAGE
  $ datocms plugins:reset [--hard] [--reinstall]

FLAGS
  --hard       Delete node_modules and package manager related files in addition to uninstalling plugins.
  --reinstall  Reinstall all plugins after uninstalling.
```

_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.5.1/src/commands/plugins/reset.ts)_

## `datocms plugins:uninstall [PLUGIN]`

Removes a plugin from the CLI.

```
USAGE
  $ datocms plugins:uninstall [PLUGIN...] [-h] [-v]

ARGUMENTS
  [PLUGIN...]  plugin to uninstall

FLAGS
  -h, --help     Show CLI help.
  -v, --verbose

DESCRIPTION
  Removes a plugin from the CLI.

ALIASES
  $ datocms plugins:unlink
  $ datocms plugins:remove

EXAMPLES
  $ datocms plugins:uninstall myplugin
```

_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.5.1/src/commands/plugins/uninstall.ts)_

## `datocms plugins:unlink [PLUGIN]`

Removes a plugin from the CLI.

```
USAGE
  $ datocms plugins:unlink [PLUGIN...] [-h] [-v]

ARGUMENTS
  [PLUGIN...]  plugin to uninstall

FLAGS
  -h, --help     Show CLI help.
  -v, --verbose

DESCRIPTION
  Removes a plugin from the CLI.

ALIASES
  $ datocms plugins:unlink
  $ datocms plugins:remove

EXAMPLES
  $ datocms plugins:unlink myplugin
```

## `datocms plugins:update`

Update installed plugins.

```
USAGE
  $ datocms plugins:update [-h] [-v]

FLAGS
  -h, --help     Show CLI help.
  -v, --verbose

DESCRIPTION
  Update installed plugins.
```

_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.5.1/src/commands/plugins/update.ts)_

## `datocms projects:list [QUERY]`

List DatoCMS projects accessible to the authenticated account

```
USAGE
  $ datocms projects:list [QUERY] [--json] [--limit <value>] [--workspace <value>]

ARGUMENTS
  [QUERY]  Fuzzy-match string. When omitted, returns up to --limit projects across all workspaces.

FLAGS
  --limit=<value>      [default: 20] Maximum number of results returned. Exact-match shortcut is not capped.
  --workspace=<value>  Restrict results to one workspace. Accepts "personal", an organization id, or an organization
                       name (case-insensitive exact match).

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  List DatoCMS projects accessible to the authenticated account

EXAMPLES
  $ datocms projects:list

  $ datocms projects:list blog

  $ datocms projects:list --workspace="Acme Corp"

  $ datocms projects:list --json
```

_See code: [src/commands/projects/list.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/projects/list.ts)_

## `datocms schema:generate FILENAME`

Generate TypeScript definitions for the schema

```
USAGE
  $ datocms schema:generate FILENAME [--json] [--config-file <value>] [--profile <value>] [--api-token <value>]
    [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [-e <value>] [-t <value>]

ARGUMENTS
  FILENAME  Output filename for the generated TypeScript definitions

FLAGS
  -e, --environment=<value>  Environment to generate schema from
  -t, --item-types=<value>   Comma-separated list of item type API keys to include (includes dependencies)

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Generate TypeScript definitions for the schema
```

_See code: [src/commands/schema/generate.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/schema/generate.ts)_

## `datocms schema:inspect [FILTER]`

Inspect DatoCMS models and modular blocks — emits JSON with models, fields, fieldsets, nested blocks, and relationships.

```
USAGE
  $ datocms schema:inspect [FILTER] [--json] [--config-file <value>] [--profile <value>] [--api-token <value>]
    [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [-e <value>] [--type
    all|models_only|blocks_only] [--fields-details basic|complete] [--include-validators] [--include-appearance]
    [--include-default-values] [--include-fieldsets] [--include-nested-blocks] [--include-referenced-models]
    [--include-embedding-models]

ARGUMENTS
  [FILTER]  Filter by API key, ID, or display name. Falls back to fuzzy search if no exact match is found. If omitted,
            all models/blocks are returned.

FLAGS
  -e, --environment=<value>        Environment to inspect
      --fields-details=<option>    [default: basic] Level of detail returned for each field. `basic` drops validators,
                                   appearance, and default values; `complete` includes everything (very verbose). For
                                   selective inclusion use the `--include-*` flags instead.
                                   <options: basic|complete>
      --include-appearance         Include field appearance configuration
      --include-default-values     Include field default values
      --include-embedding-models   For blocks only: include every model that embeds the selected blocks (direct or
                                   transitive)
      --include-fieldsets          Include UI fieldset organization
      --include-nested-blocks      Recursively include every block nested in the selected item types
      --include-referenced-models  Include models referenced by link, links, or structured_text fields
      --include-validators         Include field validators
      --type=<option>              [default: all] Restrict to models, blocks, or both
                                   <options: all|models_only|blocks_only>

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Inspect DatoCMS models and modular blocks — emits JSON with models, fields, fieldsets, nested blocks, and
  relationships.

  Without arguments, lists every model and block in the project. Pass a
  filter to narrow down by API key (e.g. "blog_post"), ID, or display
  name; if no exact match is found a fuzzy search is used.

  By default, fields are returned without validators, appearance, or
  default values. Use `--include-validators`, `--include-appearance`,
  `--include-default-values`, or `--fields-details=complete` to opt in.

  Output is TOON on stdout (compact, agent-friendly). Pass `--json` for
  JSON output that composes with `| jq` and similar.

EXAMPLES
  List every model and block in the project

    $ datocms schema:inspect

  Inspect a single model by API key

    $ datocms schema:inspect blog_post

  Only modular blocks, with fieldsets

    $ datocms schema:inspect --type=blocks_only --include-fieldsets

  Include validators and appearance for the given model

    $ datocms schema:inspect blog_post --include-validators --include-appearance

  Full detail (verbose), piped through jq

    $ datocms schema:inspect blog_post --fields-details=complete --json | jq '.[].fields[].api_key'

  Inspect a block plus every model that embeds it (directly or indirectly)

    $ datocms schema:inspect my_block --type=blocks_only --include-embedding-models
```

_See code: [src/commands/schema/inspect.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/schema/inspect.ts)_

## `datocms unlink`

Unlink the current directory from a DatoCMS project

```
USAGE
  $ datocms unlink [--json] [--config-file <value>] [--profile <value>]

FLAGS
  --profile=<value>  [default: default] Name of the profile to remove

GLOBAL FLAGS
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.

DESCRIPTION
  Unlink the current directory from a DatoCMS project
```

_See code: [src/commands/unlink.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/unlink.ts)_

## `datocms whoami`

Show the currently authenticated DatoCMS account

```
USAGE
  $ datocms whoami [--json]

GLOBAL FLAGS
  --json  Format output as json.

DESCRIPTION
  Show the currently authenticated DatoCMS account

EXAMPLES
  $ datocms whoami
```

_See code: [src/commands/whoami.ts](https://github.com/datocms/cli/blob/datocms@4.2.0/packages/cli/src/commands/whoami.ts)_
<!-- commandsstop -->

---

# DatoCMS CLI — Contentful Import Plugin

Source [github]: https://raw.githubusercontent.com/datocms/cli/main/packages/cli-plugin-contentful/README.md

DatoCMS CLI plugin to import a Contentful project into a DatoCMS project.
Read a more detailed documentation [on the website](https://www.datocms.com/docs/import-and-export/import-space-from-contentful)

<!-- toc -->
* [DatoCMS Contentful Import CLI](#datocms-contentful-import-cli)
* [Usage](#usage)
* [Commands](#commands)
* [Test](#test)
<!-- tocstop -->

<br /><br />
<a href="https://www.datocms.com/">
<img src="https://www.datocms.com/images/full_logo.svg" height="60">
</a>
<br /><br />

# Usage

```sh-session
$ npm install -g datocms
$ datocms plugins:install @datocms/cli-plugin-contentful
$ datocms contentful:import --help
```

# Commands

<!-- commands -->
* [`@datocms/cli-plugin-contentful contentful:import`](#datocmscli-plugin-contentful-contentfulimport)

## `@datocms/cli-plugin-contentful contentful:import`

Import a Contentful project into a DatoCMS project

```
USAGE
  $ @datocms/cli-plugin-contentful contentful:import [--json] [--config-file <value>] [--profile <value>] [--api-token
    <value>] [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode stdout|file|directory] [--contentful-token
    <value>] [--contentful-space-id <value>] [--contentful-environment <value>] [--autoconfirm] [--ignore-errors]
    [--skip-content] [--only-content-type <value>] [--concurrency <value>]

FLAGS
  --autoconfirm                     Automatically enter an affirmative response to all confirmation prompts, enabling
                                    the command to execute without waiting for user confirmation, like forcing the
                                    destroy of existing Contentful schema models.
  --concurrency=<value>             [default: 15] Specify the maximum number of operations to be run concurrently
  --contentful-environment=<value>  The environment you want to work with
  --contentful-space-id=<value>     Your Contentful project space ID
  --contentful-token=<value>        Your Contentful project read-only API token
  --ignore-errors                   Ignore errors encountered during import
  --log-level=<option>              Level of logging to use for the profile
                                    <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --only-content-type=<value>       Exclusively import the specified content types. Specify the content types you want
                                    to import with comma separated Contentful IDs - Example: blogPost,landingPage,author
  --skip-content                    Exclusively import the schema (models) and ignore records and assets

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Import a Contentful project into a DatoCMS project
```

_See code: [lib/commands/contentful/import.js](https://github.com/datocms/cli/blob/@datocms/cli-plugin-contentful@4.2.0/packages/cli-plugin-contentful/lib/commands/contentful/import.js)_
<!-- commandsstop -->

# Test

Unfortunately Contentful management client only accepts read-write tokens, so we cannot make testing available for everybody.

To run the tests use this command:

```
npm test
```

You can get the `CONTENTFUL_TOKEN` from the password management service

---

# DatoCMS CLI — WordPress Import Plugin

Source [github]: https://raw.githubusercontent.com/datocms/cli/main/packages/cli-plugin-wordpress/README.md

DatoCMS CLI plugin to import a WordPress site into a DatoCMS project.

<!-- toc -->
* [DatoCMS WordPress Import CLI](#datocms-wordpress-import-cli)
* [Usage](#usage)
* [Commands](#commands)
* [Development](#development)
<!-- tocstop -->

<br /><br />
<a href="https://www.datocms.com/">
<img src="https://www.datocms.com/images/full_logo.svg" height="60">
</a>
<br /><br />

# Usage

```sh-session
npm install -g datocms
datocms plugins:install @datocms/cli-plugin-wordpress
datocms wordpress:import --help
```

# Commands

<!-- commands -->
* [`@datocms/cli-plugin-wordpress wordpress:import`](#datocmscli-plugin-wordpress-wordpressimport)

## `@datocms/cli-plugin-wordpress wordpress:import`

Imports a WordPress site into a DatoCMS project

```
USAGE
  $ @datocms/cli-plugin-wordpress wordpress:import --wp-username <value> --wp-password <value> [--json] [--config-file
    <value>] [--profile <value>] [--api-token <value>] [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode
    stdout|file|directory] [--wp-json-api-url <value> | --wp-url <value>] [--autoconfirm] [--ignore-errors]
    [--concurrency <value>]

FLAGS
  --autoconfirm              Automatically enters the affirmative response to all confirmation prompts, enabling the
                             command to execute without waiting for user confirmation. Forces the destroy of existing
                             "wp_*" models.
  --concurrency=<value>      [default: 15] Maximum number of operations to be run concurrently
  --ignore-errors            Try to ignore errors encountered during import
  --wp-json-api-url=<value>  The endpoint for your WordPress install (ex. https://www.wordpress-website.com/wp-json)
  --wp-password=<value>      (required) WordPress password
  --wp-url=<value>           A URL within a WordPress REST API-enabled site (ex. https://www.wordpress-website.com)
  --wp-username=<value>      (required) WordPress username

GLOBAL FLAGS
  --api-token=<value>    Specify a custom API key to access a DatoCMS project
  --config-file=<value>  [default: ./datocms.config.json, env: DATOCMS_CONFIG_FILE] Specify a custom config file path
  --json                 Format output as json.
  --log-level=<option>   Level of logging for performed API calls
                         <options: NONE|BASIC|BODY|BODY_AND_HEADERS>
  --log-mode=<option>    Where logged output should be written to
                         <options: stdout|file|directory>
  --profile=<value>      [env: DATOCMS_PROFILE] Use settings of profile in datocms.config.js

DESCRIPTION
  Imports a WordPress site into a DatoCMS project
```

_See code: [lib/commands/wordpress/import.js](https://github.com/datocms/cli/blob/@datocms/cli-plugin-wordpress@4.2.0/packages/cli-plugin-wordpress/lib/commands/wordpress/import.js)_
<!-- commandsstop -->

# Development

Tests require a working WordPress instance with specific data in it, and will import content in a newly created DatoCMS project.

You can launch the WP instance with:

```
docker compose up
```

You can then run tests with:

```
npm test
```

To save a new dump:

```
docker compose exec db mysqldump -uwordpress -pwordpress wordpress > wp_test_data/mysql/dump.sql
```

---

# DatoCMS Plugins — Example plugins repository and SDK overview

Source [github]: https://raw.githubusercontent.com/datocms/plugins/master/README.md

This repository provides examples of real DatoCMS plugins developed using the official [DatoCMS Plugins SDK](https://www.datocms.com/docs/plugin-sdk/introduction).

### Plugins

- [AI Asset Source](https://github.com/datocms/plugins/blob/master/ai-asset-source/README.md): Generate images from a prompt (OpenAI or Google providers) and add them directly as project uploads.
- [AI Translations](https://github.com/datocms/plugins/blob/master/ai-translations/README.md): Translate field values, entire records, and full bulk batches using DeepL, OpenAI, Anthropic, or Yandex.
- [Alt Text AI](https://github.com/datocms/plugins/blob/master/alt-text-ai/README.md): Generate alt text for image uploads in a single click via the AltText.ai service.
- [Asset Localization Checker](https://github.com/datocms/plugins/blob/master/asset-localization-checker/README.md): Sidebar panel that flags which locales of a single-asset field are missing alt or title metadata.
- [Asset Optimization](https://github.com/datocms/plugins/blob/master/asset-optimization/README.md): Bulk-optimize every project upload through Imgix transformations (format, max width, quality, lossless, etc.) with a preview-only dry run.
- [Automatic Environment Backups](https://github.com/datocms/plugins/blob/master/automatic-environment-backups/README.md): Schedule automatic forks of your primary environment (daily, weekly, biweekly, or monthly) as off-site backups.
- [Block to Links](https://github.com/datocms/plugins/blob/master/block-to-links/README.md): Convert legacy embedded modular-content blocks into linked records on a chosen model.
- [Bulk Change Author](https://github.com/datocms/plugins/blob/master/bulk-change-author/README.md): Bulk action that reassigns the creator on every selected record from the collection view.
- [Character Counter](https://github.com/datocms/plugins/blob/master/character-counter/README.md): Auto-attaches to any field with a length validator and shows live character, word, and readability stats.
- [Conditional Fields](https://github.com/datocms/plugins/blob/master/conditional-fields/README.md): Show or hide one or more target fields based on the value of a boolean source field, with optional inversion.
- [Content Calendar](https://github.com/datocms/plugins/blob/master/content-calendar/README.md): Calendar view of records (publish date, schedule, last-updated, creation date) inside the DatoCMS dashboard.
- [Copy Links](https://github.com/datocms/plugins/blob/master/copy-links/README.md): Copy and paste linked records between single-link and multiple-links fields without leaving the record editor.
- [Delete Asset from Other Environments](https://github.com/datocms/plugins/blob/master/delete-asset-from-other-environments/README.md): For an unused upload, bulk-delete its copies across every other environment so CDN caches evict cleanly.
- [Delete Assets Option](https://github.com/datocms/plugins/blob/master/delete-assets-option/README.md): When deleting records, prompt the editor to also delete the assets they referenced.
- [Delete Unused Assets](https://github.com/datocms/plugins/blob/master/delete-unused-assets/README.md): One-click cleanup that bulk-deletes every project upload not referenced anywhere.
- [Disabled Field](https://github.com/datocms/plugins/blob/master/disabled-field/README.md): Field add-on that disables any field, turning it into a read-only display in the record editor.
- [Scroll to Field](https://github.com/datocms/plugins/blob/master/field-anchor-menu/README.md): (Formerly Field Anchor Menu) Sidebar table of contents that lists every field in the record form and scrolls to them on click.
- [Import/Export Schema](https://github.com/datocms/plugins/blob/master/import-export-schema/README.md): Export and import project schema (models, blocks, fields, validators) as a portable JSON document, with conflict diffing.
- [Inverse Relationships](https://github.com/datocms/plugins/blob/master/inverse-relationships/README.md): Sidebar panel that lists every record linking back to the current one (e.g., posts by an author).
- [Locale Duplicate](https://github.com/datocms/plugins/blob/master/locale-duplicate/README.md): Bulk-copy content between locales — at the field level on a single record, or across many records and models at once.
- [Lorem Ipsum Generator](https://github.com/datocms/plugins/blob/master/lorem-ipsum/README.md): Field dropdown action that generates dummy text tuned to the field's editor (string, Markdown, WYSIWYG, Structured Text).
- [Media Layouts](https://github.com/datocms/plugins/blob/master/media-layouts/README.md): Visual gallery and layout builder for collections of media, stored as JSON (single, multiple, or grid/masonry layouts).
- [Notes](https://github.com/datocms/plugins/blob/master/notes/README.md): Post-it style sticky notes for editors, attached to a JSON sidebar field on configured models.
- [Project Exporter](https://github.com/datocms/plugins/blob/master/project-exporter/README.md): Export every record (and its referenced assets) of a project as a downloadable JSON manifest plus chunked asset ZIPs.
- [Project-wide Stage Viewer](https://github.com/datocms/plugins/blob/master/project-wide-stage-viewer/README.md): Cross-model view of every record currently sitting in a given workflow stage, surfaced from the content sidebar.
- [Record Auto-save](https://github.com/datocms/plugins/blob/master/record-auto-save/README.md): Periodically auto-save the record being edited on configured models, with optional debounce and notifications.
- [Record Bin](https://github.com/datocms/plugins/blob/master/record-bin/README.md): Soft-delete and restore "trash bin" for records, with an optional Lambda runtime for long-term storage.
- [Record Comments](https://github.com/datocms/plugins/blob/master/record-comments/README.md): Leave threaded comments under a record so collaborators can discuss content in place, with optional realtime updates.
- [Rich Text TinyMCE](https://github.com/datocms/plugins/blob/master/rich-text-tinymce/README.md): TinyMCE-powered rich-text editor for multi-paragraph (`text`) fields.
- [Schema ERD](https://github.com/datocms/plugins/blob/master/schema-erd/README.md): Visualize the project schema as a Graphviz ER diagram and export it as SVG or DOT.
- [SEO Readability Analysis](https://github.com/datocms/plugins/blob/master/seo-readability-analysis/README.md): Runs YoastSEO.js SEO and readability analysis against your live frontend on every record edit.
- [Shopify Product](https://github.com/datocms/plugins/blob/master/shopify-product/README.md): Search Shopify products and embed selected ones into a string or JSON field.
- [Slug Redirects](https://github.com/datocms/plugins/blob/master/slug-redirects/README.md): Automatically log slug changes to a singleton model so your frontend can serve 301 redirects from old URLs.
- [Star Rating Editor](https://github.com/datocms/plugins/blob/master/star-rating-editor/README.md): Render integer fields as configurable star-rating widgets.
- [Table Editor](https://github.com/datocms/plugins/blob/master/table-editor/README.md): Transform any JSON field into a structured table editor with named columns and rows.
- [Tag Editor](https://github.com/datocms/plugins/blob/master/tag-editor/README.md): Transform any string or JSON field into a tag/chip editor with auto-apply rules.
- [Todo List](https://github.com/datocms/plugins/blob/master/todo-list/README.md): JSON-field-backed todo list editor — add tasks, mark complete, reorder, hide/show completed.
- [Tree-like Slugs](https://github.com/datocms/plugins/blob/master/tree-like-slugs/README.md): Slug field add-on that propagates parent slug changes to all descendant records.
- [Unsplash](https://github.com/datocms/plugins/blob/master/unsplash/README.md): Asset source that imports Unsplash images (with author and credit metadata) directly into Media.
- [Web Previews](https://github.com/datocms/plugins/blob/master/web-previews/README.md): Show frontend preview links on selected records and surface a full in-CMS visual editor (Visual tab and sidebar).
- [Yandex Translate](https://github.com/datocms/plugins/blob/master/yandex-translate/README.md): Translate fields via Yandex Translate, manually from a dropdown or via auto-apply rules on field API keys.
- [Zoned Datetime Picker](https://github.com/datocms/plugins/blob/master/zoned-datetime-picker/README.md): Datetime picker with an explicit IANA timezone selection, stored as a structured JSON field.

---

# react-datocms — React components and hooks for DatoCMS

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/README.md

![MIT](https://img.shields.io/npm/l/react-datocms?style=for-the-badge) ![MIT](https://img.shields.io/npm/v/react-datocms?style=for-the-badge) [![Build Status](https://img.shields.io/travis/datocms/react-datocms?style=for-the-badge)](https://travis-ci.org/datocms/react-datocms)

A set of components and utilities to work faster with [DatoCMS](https://www.datocms.com/) in React environments. Integrates seamlessy with DatoCMS's [GraphQL Content Delivery API](https://www.datocms.com/docs/content-delivery-api) and [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api).

# Installation

```
npm install react-datocms
```

# Documentation

This package offers different components and hooks. Please refer to one of the following pages to learn more about a specific area of interest:

* [`<RSCImage />` and `<Image />` components for responsive/progressive images](./docs/image.md)
* [`<StructuredText />` component](./docs/structured-text.md)
* [`<VideoPlayer />` component](./docs/video-player.md)
* [`<ContentLink />` component and `useContentLink()` hook for Visual Editing with click-to-edit overlays](./docs/content-link.md)
* [`useQuerySubscription()` hook for live, real-time updates of content](./docs/live-real-time-updates.md)
* [`useSiteSearch()` hook to render a DatoCMS Site Search form widget](./docs/site-search.md)
* [`renderMetaTags()` and other helpers to render social share, SEO and Favicon meta tags](./docs/meta-tags.md)

# Demos

For fully working examples take a look at our [examples directory](https://github.com/datocms/react-datocms/tree/master/examples).

Live demo: [https://react-datocms-example.netlify.app/](https://react-datocms-example.netlify.app/)

# Development

This repository contains a number of demos/examples. You can use them to locally test your changes.

```
cd examples
npm setup
npm run start
```

# Releasing (maintainers)

Every user-visible change needs a changeset: run `npx changeset` from the repo
root in the same PR, pick the bump level (`patch` is for bug fixes only, new API
surface is `minor`) and commit the file it writes under `.changeset/`. That is
where the changelog entry comes from, and it is where the bump level is decided
— not on release day. See [`.changeset/README.md`](.changeset/README.md).

To release, from an up-to-date, clean `master`, run `npm run release`. It
builds and tests, applies the pending changesets — bumping the version and
writing `CHANGELOG.md` — publishes to npm, and only then tags `vX.Y.Z`, pushes,
and creates a GitHub release whose notes come straight from that changelog
entry. An interrupted release is resumed by re-running it, never undone. Use
`npm run release:next` for a prerelease under the `next` dist-tag.

The script is
[`@datocms/release-toolchain`](https://github.com/datocms/release-toolchain),
shared with every other DatoCMS repository and pinned here by tag.

---

# React/Next.js — Responsive <Image> and <RSCImage> components

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/docs/image.md

`<RSCImage />` and `<Image />` are React components specifically designed to work flawlessly with DatoCMS's [`responsiveImage` GraphQL query](https://www.datocms.com/docs/content-delivery-api/uploads#responsive-images) which optimizes image loading for your websites.

- TypeScript ready;
- CSS-in-JS ready;
- Usable both client and server side;
- Compatible with vanilla React, Next.js and pretty much any other React-based solution;

## Out-of-the-box features

- Offers optimized version of images for browsers that support WebP/AVIF format
- Generates multiple smaller images so smartphones and tablets don’t download desktop-sized images
- Efficiently lazy loads images to speed initial page load and save bandwidth
- Holds the image position so your page doesn’t jump while images load
- Uses either blur-up or background color techniques to show a preview of the image while it loads

![](docs/image-component.gif?raw=true)

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [`<RSCImage />` vs `<Image />`](#rscimage--vs-image-)
- [Usage](#usage)
- [Example](#example)
  - [The `ResponsiveImage` object](#the-responsiveimage-object)
- [`<RSCImage>`](#rscimage)
  - [Props](#props)
- [`<Image>`](#image)
  - [Props](#props-1)
  - [Layout mode](#layout-mode)
  - [Changing `data`](#changing-data)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->


## Installation

```
npm install --save react-datocms
```

## `<RSCImage />` vs `<Image />`

Even though their purpose is the same, there are some significant differences between these two components. Depending on your specific needs, you can choose to use one or the other:

* `<RSCImage />` is a [React Server Component](https://nextjs.org/docs/app/building-your-application/rendering/server-components), so it can be rendered and optionally cached on the server. It doesn't create any JS footprint. It generates a single `<picture />` element and implements lazy-loading using the native [`loading="lazy"` attribute](https://web.dev/articles/browser-level-image-lazy-loading). The placeholder is set as the background to the image itself. Be careful: the placeholder is not removed when the image loads, so it's not recommended to use this component if you anticipate that the image may have an alpha channel with transparencies.
* `<Image />` is a [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components). Since it runs on the browser, it has the ability to set a cross-fade effect between the placeholder and the original image, but at the cost of generating more complex HTML output composed of multiple elements around the main `<picture />` element. It also implements lazy-loading through `IntersectionObserver`, which allows customization of the thresholds at which lazy loading occurs.


## Usage

1. Import `Image` or `RSCImage` from `react-datocms` and use it in place of a regular `<img />` tag
2. Write a GraphQL query to your DatoCMS project using the [`responsiveImage` query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#responsive-images)

The GraphQL query returns multiple thumbnails with optimized compression. The image components automatically set up the “blur-up” effect as well as lazy loading of images further down the screen.

## Example

For a fully working example take a look at our [examples directory](https://github.com/datocms/react-datocms/tree/master/examples).

```jsx
import React from 'react';
import { Image, RSCImage } from 'react-datocms';

const Page = ({ data }) => (
  <div>
    <h1>{data.blogPost.title}</h1>
    {/* uses native loading="lazy" */}
    <RSCImage data={data.blogPost.cover.responsiveImage} />
    {/* custom lazy-loading via IntersectionObserver */}
    <Image data={data.blogPost.cover.responsiveImage} />
  </div>
);

const query = gql`
  query {
    blogPost {
      title
      cover {
        responsiveImage(
          imgixParams: { fit: crop, w: 300, h: 300, auto: format }
        ) {
          # always required
          src
          srcSet
          width
          height

          # not required, but strongly suggested!
          alt
          title

          # blur-up placeholder, JPEG format, base64-encoded, or...
          base64
          # background color placeholder
          bgColor

          # you can omit `sizes` if you explicitly pass the `sizes` prop to the image component
          sizes
        }
      }
    }
  }
`;

export default withQuery(query)(Page);
```

### The `ResponsiveImage` object

The `data` prop of both components expects an object with the same shape as the one returned by `responsiveImage` GraphQL call. It's up to you to make a GraphQL query that will return the properties you need for a specific use of the `<Image>` component.

- The minimum required properties for `data` are: `src`, `width` and `height`;
- `alt` and `title`, while not mandatory, are all highly suggested, so remember to use them!
- If you don't request `srcSet`, the component will auto-generate an `srcset` based on `src` + the `srcSetCandidates` prop (it can help reducing the GraphQL response size drammatically when many images are returned);
- We strongly to suggest to always specify [`{ auto: format }`](https://docs.imgix.com/apis/rendering/auto/auto#format) in your `imgixParams`, instead of requesting `webpSrcSet`, so that you can also take advantage of more performant optimizations (AVIF), without increasing GraphQL response size;
- If you request both the `bgColor` and `base64` property, the latter will take precedence, so just avoid querying both fields at the same time, as it will only make the GraphQL response bigger :wink:;
- You can avoid requesting `sizes` and directly pass a `sizes` prop to the component to reduce the GraphQL response size;

Here's a complete recap of what `responsiveImage` offers:

| property   | type    | required           | description                                                                                                                                                                                    |
| ---------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| src        | string  | :white_check_mark: | The `src` attribute for the image                                                                                                                                                              |
| width      | integer | :white_check_mark: | The width of the image                                                                                                                                                                         |
| height     | integer | :white_check_mark: | The height of the image                                                                                                                                                                        |
| alt        | string  | :x:                | Alternate text (`alt`) for the image (not required, but strongly suggested!)                                                                                                                   |
| title      | string  | :x:                | Title attribute (`title`) for the image (not required, but strongly suggested!)                                                                                                                |
| sizes      | string  | :x:                | The HTML5 `sizes` attribute for the image (omit it if you're already passing a `sizes` prop to the Image component)                                                                            |
| base64     | string  | :x:                | A base64-encoded thumbnail to offer during image loading                                                                                                                                       |
| bgColor    | string  | :x:                | The background color for the image placeholder (omit it if you're already requesting `base64`)                                                                                                 |
| srcSet     | string  | :x:                | The HTML5 `srcSet` attribute for the image (can be omitted, the Image component knows how to build it based on `src`)                                                                          |
| webpSrcSet | string  | :x:                | The HTML5 `srcSet` attribute for the image in WebP format (deprecated, it's better to use the [`auto=format`](https://docs.imgix.com/apis/rendering/auto/auto#format) Imgix transform instead) |


## `<RSCImage>`

### Props

| prop             | type                     | required                     | description                                                                                                                                          | default                                                                                                                                              |
| ---------------- | ------------------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| data             | `ResponsiveImage` object | :white_check_mark:           | The actual response you get from a DatoCMS `responsiveImage` GraphQL query                            ****                                           |                                                                                                                                                      |
| pictureClassName | string                   | :x:                          | Additional className for the root `<picture>` tag                                                                                                    | null                                                                                                                                                 |
| pictureStyle     | CSS properties           | :x:                          | Additional CSS rules to add to the root `<picture>` tag                                                                                              | null                                                                                                                                                 |
| imgClassName     | string                   | :x:                          | Additional className for the `<img>` tag                                                                                                             | null                                                                                                                                                 |
| imgStyle         | CSS properties           | :x:                          | Additional CSS rules to add to the `<img>` tag                                                                                                       | null                                                                                                                                                 |
| priority         | Boolean                  | :x:                          | Disables lazy loading, and sets the image [fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) to "high" | false                                                                                                                                                |
| sizes            | string                   | :x:                          | The HTML5 [`sizes`](https://web.dev/learn/design/responsive-images/#sizes) attribute for the image (will be used `data.sizes` as a fallback)         | undefined                                                                                                                                            |
| usePlaceholder   | Boolean                  | :x:                          | Whether the image should use a blurred image placeholder                                                                                             | true                                                                                                                                                 |
| srcSetCandidates | Array<number>            | :x:                          | If `data` does not contain `srcSet`, the candidates for the `srcset` attribute of the image will be auto-generated based on these width multipliers  | [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4]                                                                                                                   |
| referrerPolicy   | string                   | `no-referrer-when-downgrade` | :x:                                                                                                                                                  | Defines which referrer is sent when fetching the image. Defaults to `no-referrer-when-downgrade` to give more useful stats in DatoCMS Project Usages |

## `<Image>`

### Props

| prop                  | type                                             | required                     | description                                                                                                                                                                                                                                                                                   | default                                                                                                                                              |
| --------------------- | ------------------------------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| data                  | `ResponsiveImage` object                         | :white_check_mark:           | The actual response you get from a DatoCMS `responsiveImage` GraphQL query                                                                                                                                                                                                                    |                                                                                                                                                      |
| layout                | 'intrinsic' \| 'fixed' \| 'responsive' \| 'fill' | :x:                          | The layout behavior of the image as the viewport changes size                                                                                                                                                                                                                                 | "intrinsic"                                                                                                                                          |
| fadeInDuration        | integer                                          | :x:                          | Duration (in ms) of the fade-in transition effect upon image loading                                                                                                                                                                                                                          | 500                                                                                                                                                  |
| intersectionThreshold | float                                            | :x:                          | Indicate at what percentage of the placeholder visibility the loading of the image should be triggered. A value of 0 means that as soon as even one pixel is visible, the callback will be run. A value of 1.0 means that the threshold isn't considered passed until every pixel is visible. | 0                                                                                                                                                    |
| intersectionMargin    | string                                           | :x:                          | Margin around the placeholder. Can have values similar to the CSS margin property (top, right, bottom, left). The values can be percentages. This set of values serves to grow or shrink each side of the placeholder element's bounding box before computing intersections.                  | "0px 0px 0px 0px"                                                                                                                                    |
| priority              | Boolean                                          | :x:                          | Disables lazy loading, and sets the image [fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) to "high"                                                                                                                                          | false                                                                                                                                                |
| sizes                 | string                                           | :x:                          | The HTML5 [`sizes`](https://web.dev/learn/design/responsive-images/#sizes) attribute for the image (will be used `data.sizes` as a fallback)                                                                                                                                                  | undefined                                                                                                                                            |
| onLoad                | () => void                                       | :x:                          | Function triggered when the image has finished loading                                                                                                                                                                                                                                        | undefined                                                                                                                                            |
| usePlaceholder        | Boolean                                          | :x:                          | Whether the component should use a blurred image placeholder                                                                                                                                                                                                                                  | true                                                                                                                                                 |
| srcSetCandidates      | Array<number>                                    | :x:                          | If `data` does not contain `srcSet`, the candidates for the `srcset` attribute of the image will be auto-generated based on these width multipliers                                                                                                                                           | [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4]                                                                                                                   |
| className             | string                                           | :x:                          | Additional CSS className for root node                                                                                                                                                                                                                                                        | null                                                                                                                                                 |
| style                 | CSS properties                                   | :x:                          | Additional CSS rules to add to the root node                                                                                                                                                                                                                                                  | null                                                                                                                                                 |
| pictureClassName      | string                                           | :x:                          | Additional CSS class for the inner `<picture />` tag                                                                                                                                                                                                                                          | null                                                                                                                                                 |
| pictureStyle          | CSS properties                                   | :x:                          | Additional CSS rules to add to the inner `<picture />` tag                                                                                                                                                                                                                                    | null                                                                                                                                                 |
| imgClassName          | string                                           | :x:                          | Additional CSS class for the image inside the `<picture />` tag                                                                                                                                                                                                                               | null                                                                                                                                                 |
| imgStyle              | CSS properties                                   | :x:                          | Additional CSS rules to add to the image inside the `<picture />` tag                                                                                                                                                                                                                         | null                                                                                                                                                 |
| placeholderClassName  | string                                           | :x:                          | Additional CSS class for the placeholder image                                                                                                                                                                                                                                                | null                                                                                                                                                 |
| placeholderStyle      | CSS properties                                   | :x:                          | Additional CSS rules for the placeholder image                                                                                                                                                                                                                                                | null                                                                                                                                                 |
| referrerPolicy        | string                                           | `no-referrer-when-downgrade` | :x:                                                                                                                                                                                                                                                                                           | Defines which referrer is sent when fetching the image. Defaults to `no-referrer-when-downgrade` to give more useful stats in DatoCMS Project Usages |

### Layout mode

With the `layout` property, you can configure the behavior of the image as the viewport changes size:

- When `intrinsic` (default behaviour), the image will scale the dimensions down for smaller viewports, but maintain the original dimensions for larger viewports.
- When `fixed`, the image dimensions will not change as the viewport changes (no responsiveness) similar to the native `img` element.
- When `responsive`, the image will scale the dimensions down for smaller viewports and scale up for larger viewports.
- When `fill`, the image will stretch both width and height to the dimensions of the parent element, provided the parent element is relative.
  - This is usually paired with the `objectFit` and `objectPosition` properties.
  - Ensure the parent element has `position: relative` in their stylesheet.

Example for `layout="fill"` (useful also for background images):

```jsx
<div style={{ position: 'relative', width: 200, height: 500 }}>
  <Image
    data={imageData}
    layout="fill"
    objectFit="cover"
    objectPosition="50% 50%"
  />
</div>
```

### Changing `data`

If the `data` prop changes over time, this component works like a regular `<img />` in a browser: the new image won't appear until it loads, while the old image stays visible. If you want the old image to disappear while loading, you can use a `key=` so that React sees the changing image as a new `<img />` instead of just changing the src attribute:

```jsx
<Image
  key={imageData.src}
  data={imageData}
/>
```

---

# React/Next.js — <StructuredText> component to render Structured Text fields

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/docs/structured-text.md

`<StructuredText />` is a React component that you can use to render the value contained inside a DatoCMS [Structured Text field type](https://www.datocms.com/docs/structured-text/dast).

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Custom renderers for inline records, blocks, and links](#custom-renderers-for-inline-records-blocks-and-links)
- [Override default rendering of nodes](#override-default-rendering-of-nodes)
- [Props](#props)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->


## Installation

```
npm install --save react-datocms
```

## Basic usage

```js
import React from 'react';
import { StructuredText } from 'react-datocms';

const Page = ({ data }) => {
  // data.blogPost.content = {
  //   value: {
  //     schema: "dast",
  //     document: {
  //       type: "root",
  //       children: [
  //         {
  //           type: "heading",
  //           level: 1,
  //           children: [
  //             {
  //               type: "span",
  //               value: "Hello ",
  //             },
  //             {
  //               type: "span",
  //               marks: ["strong"],
  //               value: "world!",
  //             },
  //           ],
  //         },
  //       ],
  //     },
  //   },
  // }

  return (
    <div>
      <h1>{data.blogPost.title}</h1>
      <StructuredText data={data.blogPost.content} />
      {/* -> <h1>Hello <strong>world!</strong></h1> */}
    </div>
  );
};

const query = gql`
  query {
    blogPost {
      title
      content {
        value
      }
    }
  }
`;

export default withQuery(query)(Page);
```

## Custom renderers for inline records, blocks, and links

You can also pass custom renderers for special nodes (inline records, record links and blocks) as an optional parameter like so:

```js
import React from 'react';
import { StructuredText, Image } from 'react-datocms';

const Page = ({ data }) => {
  // data.blogPost.content ->
  // {
  //   value: {
  //     schema: "dast",
  //     document: {
  //       type: "root",
  //       children: [
  //         {
  //           type: "heading",
  //           level: 1,
  //           children: [
  //             { type: "span", value: "Welcome onboard " },
  //             { type: "inlineItem", item: "324321" },
  //           ],
  //         },
  //         {
  //           type: "paragraph",
  //           children: [
  //             { type: "span", value: "So happy to have " },
  //             {
  //               type: "itemLink",
  //               item: "324321",
  //               children: [
  //                 {
  //                   type: "span",
  //                   marks: ["strong"],
  //                   value: "this awesome humang being",
  //                 },
  //               ]
  //             },
  //             { type: "span", value: " in our team! We call him " },
  //             { type: "inlineBlock", item: "1984560" }
  //           ]
  //         },
  //         { type: "block", item: "1984559" }
  //       ],
  //     },
  //   },
  //   links: [
  //     {
  //       id: "324321",
  //       __typename: "TeamMemberRecord",
  //       firstName: "Mark",
  //       slug: "mark-smith",
  //     },
  //   ],
  //   blocks: [
  //     {
  //       id: "1984559",
  //       __typename: "CtaRecord",
  //       title: "Call to action",
  //       url: "https://google.com"
  //     },
  //   ],
  //   inlineBlocks: [
  //     {
  //       id: "1984560",
  //       __typename: "MentionRecord",
  //       username: "steffoz",
  //     },
  //   ],
  // }

  return (
    <div>
      <h1>{data.blogPost.title}</h1>
      <StructuredText
        data={data.blogPost.content}
        renderInlineRecord={({ record }) => {
          switch (record.__typename) {
            case 'TeamMemberRecord':
              return <a href={`/team/${record.slug}`}>{record.firstName}</a>;
            default:
              return null;
          }
        }}
        renderLinkToRecord={({ record, children, transformedMeta }) => {
          switch (record.__typename) {
            case 'TeamMemberRecord':
              return (
                <a {...transformedMeta} href={`/team/${record.slug}`}>
                  {children}
                </a>
              );
            default:
              return null;
          }
        }}
        renderBlock={({ record }) => {
          switch (record.__typename) {
            case 'CtaRecord':
              return (
                <a className="button" href={record.url}>
                  {record.title}
                </a>
              );
            default:
              return null;
          }
        }}
        renderInlineBlock={({ record }) => {
          switch (record.__typename) {
            case 'MentionRecord':
              return (
                <code>
                  @{record.username}
                </code>
              );
            default:
              return null;
          }
        }}
      />
      {/*
        Final result:

        <h1>Welcome onboard <a href="/team/mark-smith">Mark</a></h1>
        <p>So happy to have <a href="/team/mark-smith">this awesome humang being</a> in our team! We call him <code>@steffoz</code></p>
        <img src="https://www.datocms-assets.com/205/1597757278-austin-distel-wd1lrb9oeeo-unsplash.jpg" alt="Our team at work" />
      */}
    </div>
  );
};

const query = gql`
  query {
    blogPost {
      title
      content {
        value
        links {
          ... on RecordInterface {
            id
            __typename
          }
          ... on TeamMemberRecord {
            firstName
            slug
          }
        }
        blocks {
          ... on RecordInterface {
            id
            __typename
          }
          ... on CtaRecord {
            title
            url
          }
        }
        inlineBlocks {
          ... on RecordInterface {
            id
            __typename
          }
          ... on MentionRecord {
            username
          }
        }
      }
    }
  }
`;

export default withQuery(query)(Page);
```

## Override default rendering of nodes

This component automatically renders all nodes (except for `inlineItem`, `itemLink`, `block` and `inlineBlock`) using a set of default rules, but you might want to customize those. For example:

For example:

- For `heading` nodes, you might want to add an anchor;
- For `code` nodes, you might want to use a custom sytax highlighting component like [`prism-react-renderer`](https://github.com/FormidableLabs/prism-react-renderer);
- Apply different logic/formatting to a node based on what its parent node is (using the `ancestors` parameter)

- For all possible node types, refer to the [list of typeguard functions defined in the main `structured-text` package](https://github.com/datocms/structured-text/tree/main/packages/utils#typescript-type-guards). The [DAST format documentation](https://www.datocms.com/docs/structured-text/dast) has additional details.

In this case, you can easily override default rendering rules with the `customNodeRules` and `customMarkRules` props.

```jsx
import { renderNodeRule, renderMarkRule, StructuredText } from 'react-datocms';
import { isHeading, isCode } from 'datocms-structured-text-utils';
import { render as toPlainText } from 'datocms-structured-text-to-plain-text';
import SyntaxHighlight from 'components/SyntaxHighlight';

<StructuredText
  data={data.blogPost.content}
  customNodeRules={[
    // Add HTML anchors to heading levels for in-page navigation
    renderNodeRule(isHeading, ({ node, children, key }) => {
      const HeadingTag = `h${node.level}`;
      const anchor = toPlainText(node)
        .toLowerCase()
        .replace(/ /g, '-')
        .replace(/[^\w-]+/g, '');

      return (
        <HeadingTag key={key}>
          {children} <a id={anchor} />
          <a href={`#${anchor}`} />
        </HeadingTag>
      );
    }),

    // Use a custom syntax highlighter component for code blocks
    renderNodeRule(isCode, ({ node, key }) => {
      return (
        <SyntaxHighlight
          key={key}
          code={node.code}
          language={node.language}
          linesToBeHighlighted={node.highlight}
        />
      );
    }),

    // Apply different formatting to top-level paragraphs
    renderNodeRule(
      isParagraph,
      ({ adapter: { renderNode }, node, children, key, ancestors }) => {
        if (isRoot(ancestors[0])) {
          // If this paragraph node is a top-level one, give it a special class
          return renderNode(
            'p',
            { key, className: 'top-level-paragraph-container-example' },
            children,
          );
        } else {
          // Proceed with default paragraph rendering...
          // return renderNode('p', { key }, children);

          // Or even completely remove the paragraph and directly render the inner children:
          return <React.Fragment key={key}>{children}</React.Fragment>;
        }
      },
    ),
  ]}
  customMarkRules={[
    // convert "strong" marks into <b> tags
    renderMarkRule('strong', ({ mark, children, key }) => {
      return <b key={key}>{children}</b>;
    }),
  ]}
/>;
```

Note: if you override the rules for `inlineItem`, `itemLink`, `block` or `inlineBlock` nodes, then the `renderInlineRecord`, `renderLinkToRecord`, `renderBlock` and `renderInlineBlock` props won't be considered!

## Props

| prop               | type                                                            | required                                               | description                                                                                      | default                                                                                                              |
| ------------------ | --------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| data               | `StructuredTextGraphQlResponse \| DastNode`                     | :white_check_mark:                                     | The actual [field value](https://www.datocms.com/docs/structured-text/dast) you get from DatoCMS |                                                                                                                      |
| renderInlineRecord | `({ record }) => ReactElement \| null`                          | Only required if document contains `inlineItem` nodes  | Convert an `inlineItem` DAST node into React                                                     | `[]`                                                                                                                 |
| renderLinkToRecord | `({ record, children }) => ReactElement \| null`                | Only required if document contains `itemLink` nodes    | Convert an `itemLink` DAST node into React                                                       | `null`                                                                                                               |
| renderBlock        | `({ record }) => ReactElement \| null`                          | Only required if document contains `block` nodes       | Convert a `block` DAST node into React                                                           | `null`                                                                                                               |
| renderInlineBlock  | `({ record }) => ReactElement \| null`                          | Only required if document contains `inlineBlock` nodes | Convert an `inlineBlock` DAST node into React                                                    | `null`                                                                                                               |
| metaTransformer    | `({ node, meta }) => Object \| null`                            | :x:                                                    | Transform `link` and `itemLink` meta property into HTML props                                    | [See function](https://github.com/datocms/structured-text/blob/main/packages/generic-html-renderer/src/index.ts#L61) |
| customNodeRules    | `Array<RenderRule>`                                             | :x:                                                    | Customize how nodes are converted in JSX (use `renderNodeRule()` to generate rules)              | `null`                                                                                                               |
| customMarkRules    | `Array<RenderMarkRule>`                                         | :x:                                                    | Customize how marks are converted in JSX (use `renderMarkRule()` to generate rules)              | `null`                                                                                                               |
| renderText         | `(text: string, key: string) => ReactElement \| string \| null` | :x:                                                    | Convert a simple string text into React                                                          | `(text) => text`                                                                                                     |

---

# React/Next.js — <VideoPlayer> component for Mux-encoded videos

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/docs/video-player.md

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [`<VideoPlayer/>` component for easy video integration.](#videoplayer-component-for-easy-video-integration)
  - [Out-of-the-box features](#out-of-the-box-features)
  - [Installation](#installation)
  - [Usage](#usage)
  - [Example](#example)
  - [Props](#props)
  - [Advanced usage: the `useVideoPlayer` hook](#advanced-usage-the-usevideoplayer-hook)
    - [Example](#example-1)
  - [Opt-in Viewer Analytics](#opt-in-viewer-analytics)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->


`<VideoPlayer />` is a React component specially designed to work seamlessly with DatoCMS’s [`video` GraphQL query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#videos) that optimizes video streaming for your sites.

To stream videos, DatoCMS partners with MUX, a video CDN that serves optimized streams to your users. Our component is a wrapper over MUX's video player for React. It takes care of the details for you, and this is our recommended way to serve optimal videos to your users.

## Out-of-the-box features

- Offers optimized streaming so smartphones and tablets don’t request desktop-sized videos
- Lazy loads the video component and the video to be played to speed initial page load and save bandwidth
- Holds the video position and size so your page doesn’t jump while the player loads
- Uses blur-up technique to show a placeholder of the video while it loads

## Installation

```
npm install --save react-datocms @mux/mux-player-react
```

`@mux/mux-player-react` is a [peer dependency](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependencies) for `react-datocms`: so you're expected to add it in your project.

## Usage

1. Import `VideoPlayer` from `react-datocms` and use it in your app
2. Write a GraphQL query to your DatoCMS project using the [`video` query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#videos)

The GraphQL query returns data that the `VideoPlayer` component automatically uses to properly size the player, set up a “blur-up” placeholder as well as lazy loading the video.

## Example

For a fully working example take a look at our [examples directory](https://github.com/datocms/react-datocms/tree/master/examples).

```js
import React from 'react';
import { VideoPlayer } from 'react-datocms';

const Page = ({ data }) => (
  <div>
    <h1>{data.blogPost.title}</h1>
    <VideoPlayer data={data.blogPost.cover.video} />
  </div>
);

const query = gql`
  query {
    blogPost {
      title
      cover {
        video {
          # required: this field identifies the video to be played
          muxPlaybackId

          # all the other fields are not required but:

          # if provided, title is displayed in the upper left corner of the video
          title

          # if provided, width and height are used to define the aspect ratio of the
          # player, so to avoid layout jumps during the rendering.
          width
          height

          # if provided, it shows a blurred placeholder for the video
          blurUpThumb

          # if provided, it enables DatoCMS Content Link for click-to-edit overlays
          alt

          # you can include more data here: they will be ignored by the component
        }
      }
    }
  }
`;

export default withQuery(query)(Page);
```

## Props

The `<VideoPlayer />` components supports all [the properties made
available](https://github.com/muxinc/elements/blob/main/packages/mux-player-react/REFERENCE.md)
for `<MuxPlayer />` component from `@mux/mux-player-react` package plus `data`,
which is meant to receive data directly in the shape they are provided by
DatoCMS GraphQL API.

`<Video Player />` uses the `data` prop to generate a set of props for the inner
`<MuxPlayer />`. On this topic, also see the "Advanced usage" section below.

| prop | type           | required           | description                                                      | default |
| ---- | -------------- | ------------------ | ---------------------------------------------------------------- | ------- |
| data | `Video` object | :white_check_mark: | The actual response you get from a DatoCMS `video` GraphQL query |         |

Compared to the `<MuxPlayer />`, **some prop default values are different** on `<VideoPlayer />`

- `disableCookies` is normally true, unless you explicitly set the prop to `false`
- `disableTracking` is normally true, unless you explicitly set it to `false`
- `preload` defaults to `metadata`, for an optimal UX experience together with saved bandwidth
- the video height and width, when available in the `data` props, are used to set `{ aspectRatio: "[width] / [height]"}` for the `<MuxPlayer />`'s `style`

All the other props are forwarded to the `<MuxPlayer />` component that is used internally.

## Advanced usage: the `useVideoPlayer` hook

Even though we try our best to make the `<VideoPlayer />` suitable and easy to use for most normal use cases, there are situations where you may need to leverage the `<MuxPlayer />` directly (let's suppose you wrote your special wrapper component around the `<MuxPlayer />` and you need a bunch of props to pass). If that's the case, fill free to use the hook we provide: `useVideoPlayer`.

`useVideoPlayer` takes data coming in the shape they are produced from DatoCMS API and return props that you can pass to the `<MuxPlayer />` component. That's pretty much what the `<VideoPlayer />` does internally.

### Example

```
import { useVideoPlayer } from 'react-datocms';

const data = {
  muxPlaybackId: 'ip028MAXF026dU900bKiyNDttjonw7A1dFY',
  title: 'Title',
  width: 1080,
  height: 1920,
  blurUpThumb:
    'data:image/bmp;base64,Qk0eAAAAAAAAABoAAAAMAAAAAQABAAEAGAAAAP8A',
};

// `props` is the following object:
//
//     {
//        playbackId: 'ip028MAXF026dU900bKiyNDttjonw7A1dFY',
//        title: 'Title',
//        style: {
//          aspectRatio: '1080 / 1920',
//        },
//        placeholder:
//          'data:image/bmp;base64,Qk0eAAAAAAAAABoAAAAMAAAAAQABAAEAGAAAAP8A',
//      }
const props = useVideoPlayer({ data });

<MuxPlayer {...props} />
```

## Opt-in Viewer Analytics

This `<VideoPlayer/>` component can OPTIONALLY collect clientside [playback and engagement metrics](https://www.mux.com/data#TechSpecs) such as playback percentages, user agents, and geography.

These analytics are **disabled** by default. To enable them, you must opt in to [Mux Data](https://www.mux.com/data) integration by creating a Mux Data account (free) and providing its `envKey` to the component.

For details and setup instructions, please see our documentation on **[Streaming Video Analytics with Mux Data](https://www.datocms.com/docs/streaming-videos/streaming-video-analytics-with-mux-data)**.

---

# React/Next.js — useQuerySubscription hook for live real-time updates

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/docs/live-real-time-updates.md

`useQuerySubscription` is a React hook that you can use to implement client-side updates of the page as soon as the content changes. It uses DatoCMS's [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api/api-reference) to receive the updated query results in real-time, and is able to reconnect in case of network failures.

Live updates are great both to get instant previews of your content while editing it inside DatoCMS, or to offer real-time updates of content to your visitors (ie. news site).

- TypeScript ready;
- Compatible with vanilla React, Next.js and pretty much any other React-based solution;

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Reference](#reference)
- [Initialization options](#initialization-options)
- [Connection status](#connection-status)
- [Error object](#error-object)
- [Example](#example)
- [The `fetcher` option](#the-fetcher-option)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

```
npm install --save react-datocms
```

## Reference

Import `useQuerySubscription` from `react-datocms` and use it inside your components like this:

```js
const {
  data: QueryResult | undefined,
  error: ChannelErrorData | null,
  status: ConnectionStatus,
} = useQuerySubscription(options: Options);
```

## Initialization options

| prop               | type                                                                                       | required           | description                                                                                      | default                              |
| ------------------ | ------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------ |
| enabled            | boolean                                                                                    | :x:                | Whether the subscription has to be performed or not                                              | true                                 |
| query              | string \| [`TypedDocumentNode`](https://github.com/dotansimha/graphql-typed-document-node) | :white_check_mark: | The GraphQL query to subscribe                                                                   |                                      |
| token              | string                                                                                     | :white_check_mark: | DatoCMS API token to use                                                                         |                                      |
| variables          | Object                                                                                     | :x:                | GraphQL variables for the query                                                                  |                                      |
| includeDrafts      | boolean                                                                                    | :x:                | If true, draft records will be returned                                                          |                                      |
| excludeInvalid     | boolean                                                                                    | :x:                | If true, invalid records will be filtered out                                                    |                                      |
| environment        | string                                                                                     | :x:                | The name of the DatoCMS environment where to perform the query (defaults to primary environment) |                                      |
| contentLink        | `'vercel-1'` or `undefined`                                                                | :x:                | If true, embed metadata that enable Content Link                                                 |                                      |
| baseEditingUrl     | string                                                                                     | :x:                | The base URL of the DatoCMS project                                                              |                                      |
| cacheTags          | boolean                                                                                    | :x:                | If true, receive the Cache Tags associated with the query                                        |                                      |
| initialData        | Object                                                                                     | :x:                | The initial data to use on the first render                                                      |                                      |
| reconnectionPeriod | number                                                                                     | :x:                | In case of network errors, the period (in ms) to wait to reconnect                               | 1000                                 |
| fetcher            | a [fetch-like function](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)        | :x:                | The fetch function to use to perform the registration query                                      | window.fetch                         |
| eventSourceClass   | an [EventSource-like](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) class  | :x:                | The EventSource class to use to open up the SSE connection                                       | window.EventSource                   |
| baseUrl            | string                                                                                     | :x:                | The base URL to use to perform the query                                                         | `https://graphql-listen.datocms.com` |

## Connection status

The `status` property represents the state of the server-sent events connection. It can be one of the following:

- `connecting`: the subscription channel is trying to connect
- `connected`: the channel is open, we're receiving live updates
- `closed`: the channel has been permanently closed due to a fatal error (ie. an invalid query)

## Error object

| prop     | type   | description                                             |
| -------- | ------ | ------------------------------------------------------- |
| code     | string | The code of the error (ie. `INVALID_QUERY`)             |
| message  | string | An human friendly message explaining the error          |
| response | Object | The raw response returned by the endpoint, if available |

## Example

```js
import React from 'react';
import { useQuerySubscription } from 'react-datocms';

const App: React.FC = () => {
  const { status, error, data } = useQuerySubscription({
    enabled: true,
    query: `
      query AppQuery($first: IntType) {
        allBlogPosts {
          slug
          title
        }
      }`,
    variables: { first: 10 },
    token: 'YOUR_API_TOKEN',
  });

  const statusMessage = {
    connecting: 'Connecting to DatoCMS...',
    connected: 'Connected to DatoCMS, receiving live updates!',
    closed: 'Connection closed',
  };

  return (
    <div>
      <p>Connection status: {statusMessage[status]}</p>
      {error && (
        <div>
          <h1>Error: {error.code}</h1>
          <div>{error.message}</div>
          {error.response && (
            <pre>{JSON.stringify(error.response, null, 2)}</pre>
          )}
        </div>
      )}
      {data && (
        <ul>
          {data.allBlogPosts.map((blogPost) => (
            <li key={blogPost.slug}>{blogPost.title}</li>
          ))}
        </ul>
      )}
    </div>
  );
};
```

## The `fetcher` option

Be careful with how you define the `fetcher` option: use a function that is
defined as a `const` outside of the lexical scope where you're using
`useQuerySubscription`.

If you don't, you could have an infinite render loop, because the function looks
like new at every render of the component. For more info, see
[use-deep-compare-effect](https://github.com/kentcdodds/use-deep-compare-effect?tab=readme-ov-file#usage)
documentation.

The following example is ok:

```js
const fetcher = (baseUrl, { headers, method, body }) => {
  return fetch(baseUrl, {
    headers: {
      ...headers,
      'X-Custom-Header': "that's needed for some reason",
    },
    method,
    body,
  });
};

export default function Home() {
  const { status, error, data } = useQuerySubscription({
    fetcher,
    // Other options here
  });

  return ...
}
```

**This one is not**, because the new function that is generated every time the component is rendered triggers another render: 

```js
export default function Home() {
  const { status, error, data } = useQuerySubscription({
    fetcher: (baseUrl, { headers, method, body }) => {
      return fetch(baseUrl, {
        headers: {
          ...headers,
          'X-Custom-Header': "that's needed for some reason",
        },
        method,
        body,
      });
    },
    // Other options here
  });

  return ...
}
```

---

# React/Next.js — useSiteSearch hook to query the DatoCMS Site Search API

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/docs/site-search.md

`useSiteSearch` is a React hook that you can use to render a [DatoCMS Site Search](https://www.datocms.com/docs/site-search) widget.
The hook only handles the form logic: you are in complete and full control of how your form renders down to the very last component, class or style.

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Reference](#reference)
- [Initialization options](#initialization-options)
- [Returned data](#returned-data)
- [Complete example](#complete-example)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

To perform the necessary API requests, this hook requires a [DatoCMS CMA Client](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients) instance, so make sure to also add the following package to your project:

```bash
npm install --save react-datocms @datocms/cma-client-browser
```

## Reference

Import `useSiteSearch` from `react-datocms` and use it inside your components like this:

```js
import { useSiteSearch } from 'react-datocms';
import { buildClient } from '@datocms/cma-client-browser';

const client = buildClient({ apiToken: 'YOUR_API_TOKEN' });

const { state, error, data } = useSiteSearch({
  client,
  searchIndexId: '7497',
  // optional: by default fuzzy-search is not active
  fuzzySearch: true,
  // optional: you can omit it you only have one locale, or you want to find results in every locale
  initialState: { locale: 'en' },
  // optional: to configure how to present the part of page title/content that matches the query
  highlightMatch: (text, key, context) =>
    context === 'title' ? (
      <strong key={key}>{text}</strong>
    ) : (
      <mark key={key}>{text}</mark>
    ),
  // optional: defaults to 8 search results per page
  resultsPerPage: 10,
});
```

For a complete walk-through, please refer to the [DatoCMS Site Search documentation](https://www.datocms.com/docs/site-search).

## Initialization options

| prop                | type                                                               | required           | description                                                                                                                                | default                                                    |
| ------------------- | ------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| client              | CMA Client instance                                                | :white_check_mark: | [DatoCMS CMA Client](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients) instance                                |                                                            |
| searchIndexId      | string                                                             | :white_check_mark: | The [ID of the the search index](https://www.datocms.com/docs/site-search/base-integration#performing-searches) to use to find search results |                                                            |
| fuzzySearch         | boolean                                                            | :x:                | Whether fuzzy-search is active or not. When active, it will also find strings that approximately match the query provided.                 | false                                                      |
| resultsPerPage      | number                                                             | :x:                | The number of search results to show per page                                                                                              | 8                                                          |
| highlightMatch      | (match, key, context: 'title' \| 'bodyExcerpt') => React.ReactNode | :x:                | A function specifying how to highlight the part of page title/content that matches the query                                               | (text, key) => (&lt;mark key={key}&gt;{text}&lt;/mark&gt;) |
| initialState.query  | string                                                             | :x:                | Initialize the form with a specific query                                                                                                  | ''                                                         |
| initialState.locale | string                                                             | :x:                | Initialize the form starting from a specific page                                                                                          | 0                                                          |
| initialState.page   | string                                                             | :x:                | Initialize the form with a specific locale selected                                                                                        | null                                                       |

## Returned data

The hook returns an object with the following shape:

```typescript
{
  state: {
    query: string;
    setQuery: (newQuery: string) => void;
    locale: string | undefined;
    setLocale: (newLocale: string) => void;
    page: number;
    setPage: (newPage: number) => void;
  },
  error?: string,
  data?: {
    pageResults: Array<{
      id: string;
      title: React.ReactNode;
      bodyExcerpt: React.ReactNode;
      url: string;
      raw: RawSearchResult;
    }>;
    totalResults: number;
    totalPages: number;
  },
}
```

- The `state` property reflects the current state of the form (the current `query`, `page`, and `locale`), and offers a number of functions to change the state itself. As soon as the state of the form changes, a new API request is made to fetch the new search results;
- The `error` property returns a string in case of failure of any API request;
- The `data` property returns all the information regarding the current search results to present to the user;

If both `error` and `data` are `null`, it means that the current state for the form is loading, and a spinner should be shown to the end user.

## Complete example

This example uses the [`react-paginate`](https://www.npmjs.com/package/react-paginate) npm package to simplify the handling of pagination:

```jsx
import { buildClient } from '@datocms/cma-client-browser';
import ReactPaginate from 'react-paginate';
import { useSiteSearch } from 'react-datocms';
import { useState } from 'react';

const client = buildClient({ apiToken: 'YOUR_API_TOKEN' });

function App() {
  const [query, setQuery] = useState('');

  const { state, error, data } = useSiteSearch({
    client,
    initialState: { locale: 'en' },
    highlightMatch: (text, key, context) =>
      context === 'title' ? (
        <strong key={key}>{text}</strong>
      ) : (
        <mark key={key}>{text}</mark>
      ),
    searchIndexId: '7497',
    resultsPerPage: 10,
  });

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          state.setQuery(query);
        }}
      >
        <input
          type="search"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
        />
        <select
          value={state.locale}
          onChange={(e) => {
            state.setLocale(e.target.value);
          }}
        >
          <option value="en">English</option>
          <option value="it">Italian</option>
        </select>
      </form>
      {!data && !error && <p>Loading...</p>}
      {error && <p>Error! {error}</p>}
      {data && (
        <>
          {data.pageResults.map((result) => (
            <div key={result.id}>
              <a href={result.url}>{result.title}</a>
              <div>{result.bodyExcerpt}</div>
              <div>{result.url}</div>
            </div>
          ))}
          <p>Total results: {data.totalResults}</p>
          <ReactPaginate
            pageCount={data.totalPages}
            forcePage={state.page}
            onPageChange={({ selected }) => {
              state.setPage(selected);
            }}
            activeClassName="active"
            renderOnZeroPageCount={() => null}
          />
        </>
      )}
    </div>
  );
}
```

---

# React/Next.js — renderMetaTags() helpers for SEO meta and favicon tags

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/docs/meta-tags.md

Just like for the [image component](./image.md) this package offers a number of utilities designed to work seamlessly with DatoCMS’s [`_seoMetaTags` and `faviconMetaTags` GraphQL queries](https://www.datocms.com/docs/content-delivery-api/seo) so that you can easily handle SEO, social shares and favicons in your pages.

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [General usage](#general-usage)
- [`renderMetaTags()`](#rendermetatags)
- [`renderMetaTagsToString()`](#rendermetatagstostring)
- [`toRemixMeta()`](#toremixmeta)
  - [For Remix v1: `toRemixV1Meta()`](#for-remix-v1-toremixv1meta)
- [`toNextMetadata()`](#tonextmetadata)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->


## Installation

```
npm install --save react-datocms
```

## General usage

All the utilities take an array of `SeoOrFaviconTag`s in the exact form they're returned by the following [DatoCMS GraphQL API queries](https://www.datocms.com/docs/content-delivery-api/seo):

- `_seoMetaTags` (always available on any type of record)
- `faviconMetaTags` on the global `_site` object.

```graphql
query {
  page: homepage {
    title
    seo: _seoMetaTags {
      attributes
      content
      tag
    }
  }

  site: _site {
    favicon: faviconMetaTags {
      attributes
      content
      tag
    }
  }
}
```

You can then concat those two arrays of tags and pass them togheter to the function, ie:

```js
renderMetaTags([...data.page.seo, ...data.site.favicon]);
```

## `renderMetaTags()`

This function generates React `<meta>` and `<link />` elements, so it is compatible with React packages like [`react-helmet`](https://www.npmjs.com/package/react-helmet).

```js
import React from 'react';
import { renderMetaTags } from 'react-datocms';
import { Helmet } from 'react-helmet';

function Page({ data }) {
  return (
    <div>
      <Helmet>
        {renderMetaTags([...data.page.seo, ...data.site.favicon])}
      </Helmet>
    </div>
  );
}
```

In React 19+, you can also directly use meta tags in JSX without any external libraries: https://react.dev/blog/2024/12/05/react-19#support-for-metadata-tags

```js
import React from 'react';
import { renderMetaTags } from 'react-datocms';

function Page({ data }) {
  return (
    <div>
        {
            renderMetaTags([...data.page.seo, ...data.site.favicon])
            // returns an array of JSX elements like
            // <title/>, <link/>, <meta/>, etc.
        } 
    </div>
  );
}
```

For a complete React 19 example, take a look at our [examples directory](https://github.com/datocms/react-datocms/tree/master/examples).


## `renderMetaTagsToString()`

This function generates an HTML string containing `<meta>` and `<link />` tags, so it can be used server-side.

```js
import { renderMetaTagsToString } from 'react-datocms';

const someMoreComplexHtml = `
  <html>
    <head>
      ${renderMetaTagsToString([...data.page.seo, ...data.site.favicon])}
    </head>
  </html>
`;
```

## `toRemixMeta()`

This function generates an array of `MetaDescriptor` objects, compatibile with the [`meta`](https://remix.run/docs/en/2.8.1/route/meta) export of the Remix framework:

```js
import type { MetaFunction } from 'remix';
import { toRemixV1Meta } from 'react-datocms';

export const meta: MetaFunction = ({ data: { post } }) => {
  return toRemixV1Meta(post.seo);
};
```

Please note that the [`links`](https://remix.run/docs/en/v1.1.1/api/conventions#links) export [doesn't receive any loader data](https://github.com/remix-run/remix/issues/32), so you cannot use it to declare favicons meta tags!

The best way to render them is using the [`meta`](https://remix.run/docs/en/2.8.1/route/meta) export as the SEO meta tags, or (even better) using `renderMetaTags` in your root component:

```jsx
import { renderMetaTags } from 'react-datocms';

export const loader = () => {
  return request({
    query: `
        {
          site: _site {
            favicon: faviconMetaTags(variants: [icon, msApplication, appleTouchIcon]) {
              ...metaTagsFragment
            }
          }
        }
        ${metaTagsFragment}
      `,
  });
};

export default function App() {
  const { site } = useLoaderData();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
        {renderMetaTags(site.favicon)}
      </head>
      <body>
        <Outlet />
        ...
      </body>
    </html>
  );
}
```

### For Remix v1: `toRemixV1Meta()`

If you're using Remix v1, you can use `toRemixV1Meta()` to generate an object compatible with the legacy [`meta`](https://remix.run/docs/en/v1.1.1/api/conventions#meta) export:

```js
import type { MetaFunction } from 'remix';
import { toRemixV1Meta } from 'react-datocms';

export const meta: MetaFunction = ({ data: { post } }) => {
  return toRemixV1Meta(post.seo);
};
```

## `toNextMetadata()`

This function generates a `Metadata` object, compatibile with the [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) export of the [Next](https://nextjs.org/) framework:

```js
export async function generateMetadata(): Promise<Metadata> {
  const { homepage } = await getHomepageContent()
 
  return toNextMetadata(homepage?._seoMetaTags || [])
}
```

---

# React/Next.js — <ContentLink> component and useContentLink hook for Visual Editing

Source [github]: https://raw.githubusercontent.com/datocms/react-datocms/master/docs/content-link.md

`<ContentLink />` is a React component that enables **Visual Editing** for your DatoCMS content. It allows content editors to click directly on content in your website preview to edit it in the DatoCMS interface, making content management intuitive and efficient.

Visual Editing works by:
- Detecting stega-encoded metadata embedded in your content
- Creating interactive overlays on editable content
- Integrating with the DatoCMS [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) for seamless editing
- Supporting keyboard shortcuts (Alt/Option key) for temporary click-to-edit mode
- Providing bidirectional communication between your preview and the DatoCMS editor

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [What is Visual Editing?](#what-is-visual-editing)
- [Out-of-the-box features](#out-of-the-box-features)
- [Installation](#installation)
- [Basic Setup](#basic-setup)
  - [1. Fetch content with stega encoding](#1-fetch-content-with-stega-encoding)
  - [2. Add the ContentLink component](#2-add-the-contentlink-component)
- [Framework integrations](#framework-integrations)
  - [Next.js App Router](#nextjs-app-router)
  - [React Router](#react-router)
- [Enabling click-to-edit](#enabling-click-to-edit)
  - [1. Via prop (persistent)](#1-via-prop-persistent)
  - [2. Via keyboard shortcut (temporary)](#2-via-keyboard-shortcut-temporary)
- [Flash-all highlighting](#flash-all-highlighting)
- [Props](#props)
- [Advanced usage: the `useContentLink` hook](#advanced-usage-the-usecontentlink-hook)
  - [Hook API](#hook-api)
  - [Example: Custom editing toolbar](#example-custom-editing-toolbar)
  - [Example: Conditional editing in different environments](#example-conditional-editing-in-different-environments)
- [Data attributes reference](#data-attributes-reference)
  - [Developer-specified attributes](#developer-specified-attributes)
    - [`data-datocms-content-link-url`](#data-datocms-content-link-url)
    - [`data-datocms-content-link-source`](#data-datocms-content-link-source)
    - [`data-datocms-content-link-group`](#data-datocms-content-link-group)
    - [`data-datocms-content-link-boundary`](#data-datocms-content-link-boundary)
  - [Library-managed attributes](#library-managed-attributes)
    - [`data-datocms-contains-stega`](#data-datocms-contains-stega)
    - [`data-datocms-auto-content-link-url`](#data-datocms-auto-content-link-url)
- [How group and boundary resolution works](#how-group-and-boundary-resolution-works)
- [Structured Text fields](#structured-text-fields)
  - [Rule 1: Always wrap the Structured Text component in a group](#rule-1-always-wrap-the-structured-text-component-in-a-group)
  - [Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary](#rule-2-wrap-embedded-blocks-inline-records-and-inline-blocks-in-a-boundary)
- [Low-level utilities](#low-level-utilities)
  - [`decodeStega`](#decodestega)
  - [`stripStega`](#stripstega)
  - [`revealStega`](#revealstega)
- [Troubleshooting](#troubleshooting)
  - [Click-to-edit overlays not appearing](#click-to-edit-overlays-not-appearing)
  - [Navigation not syncing with Web Previews plugin](#navigation-not-syncing-with-web-previews-plugin)
  - [StructuredText blocks not clickable](#structuredtext-blocks-not-clickable)
  - [Layout issues caused by stega encoding](#layout-issues-caused-by-stega-encoding)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## What is Visual Editing?

Visual Editing transforms how content editors interact with your website. Instead of navigating through forms and fields in a CMS, editors can:

1. **See their content in context** - Preview exactly how content appears on the live site
2. **Click to edit** - Click directly on any text, image, or field to open the editor
3. **Navigate seamlessly** - Jump between pages in the preview, and the CMS follows along
4. **Get instant feedback** - Changes in the CMS are reflected immediately in the preview

This drastically improves the editing experience, especially for non-technical users who can now edit content without understanding the underlying CMS structure.

## Out-of-the-box features

- **Click-to-edit overlays**: Visual indicators showing which content is editable
- **Stega decoding**: Automatically detects and decodes editing metadata embedded in content
- **Keyboard shortcuts**: Hold Alt/Option to temporarily enable editing mode
- **Flash-all highlighting**: Show all editable areas at once for quick orientation
- **Bidirectional navigation**: Sync navigation between preview and DatoCMS editor
- **Framework-agnostic**: Works with Next.js, React Router, Remix, or any routing solution
- **StructuredText integration**: Special support for complex structured content fields
- **[Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) integration**: Seamless integration with DatoCMS's editing interface

## Installation

```bash
npm install --save react-datocms
```

The package includes `@datocms/content-link` as a dependency, which provides the underlying controller for Visual Editing functionality.

## Basic Setup

Visual Editing requires two steps:

### 1. Fetch content with stega encoding

When fetching content from DatoCMS, enable stega encoding to embed editing metadata:

```js
import { executeQuery } from '@datocms/cda-client';

const query = `
  query {
    page {
      title
      content
    }
  }
`;

const result = await executeQuery(query, {
  token: 'YOUR_API_TOKEN',
  environment: 'main',
  // Enable stega encoding
  contentLink: 'v1',
  // Set your site's base URL for editing links
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
```

The `contentLink: 'v1'` option enables stega encoding, which embeds invisible metadata into text fields. The `baseEditingUrl` tells DatoCMS where your project is located so edit URLs can be generated correctly. Both options are required.

### 2. Add the ContentLink component

Add the `<ContentLink />` component to your app (typically in a root layout or provider):

```jsx
import { ContentLink } from 'react-datocms';

function App() {
  return (
    <>
      <ContentLink />
      {/* Your content */}
    </>
  );
}
```

That's it! The component will automatically scan your page for encoded content and enable Visual Editing.

## Framework integrations

For full [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) integration, provide navigation callbacks to sync the preview with the CMS:

### Next.js App Router

```jsx
'use client';

import { ContentLink as DatoContentLink } from 'react-datocms';
import { useRouter, usePathname } from 'next/navigation';

export function ContentLink() {
  const router = useRouter();
  const pathname = usePathname();

  return (
    <DatoContentLink
      onNavigateTo={(path) => router.push(path)}
      currentPath={pathname}
    />
  );
}
```

Then include this in your root layout:

```jsx
// app/layout.tsx
import { ContentLink } from './ContentLink';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ContentLink />
        {children}
      </body>
    </html>
  );
}
```

### React Router

```jsx
import { ContentLink as DatoContentLink } from 'react-datocms';
import { useNavigate, useLocation } from 'react-router-dom';

export function ContentLink() {
  const navigate = useNavigate();
  const location = useLocation();

  return (
    <DatoContentLink
      onNavigateTo={(path) => navigate(path)}
      currentPath={location.pathname}
    />
  );
}
```

## Enabling click-to-edit

There are two ways to enable click-to-edit mode:

### 1. Via prop (persistent)

```jsx
<ContentLink enableClickToEdit={true} />
```

Or with additional options:

```jsx
// Scroll to nearest editable element if none is visible
<ContentLink enableClickToEdit={{ scrollToNearestTarget: true }} />

// Only enable on devices with hover capability (non-touch)
<ContentLink enableClickToEdit={{ hoverOnly: true }} />

// Combine both options
<ContentLink enableClickToEdit={{ hoverOnly: true, scrollToNearestTarget: true }} />
```

**Available options (`ClickToEditOptions`):**

| Option                  | Type      | Default | Description                                                                                                                                                                                                            |
| ----------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scrollToNearestTarget` | `boolean` | `false` | Automatically scroll to the nearest editable element if none is currently visible in the viewport                                                                                                                      |
| `hoverOnly`             | `boolean` | `false` | Only enable click-to-edit on devices that support hover (non-touch). Uses `window.matchMedia('(hover: hover)')` to detect hover capability. On touch-only devices, users can still toggle manually with Alt/Option key |

This enables click-to-edit overlays immediately and keeps them visible.

### 2. Via keyboard shortcut (temporary)

Users can hold the **Alt** key (Windows/Linux) or **Option** key (Mac) to temporarily show click-to-edit overlays. This is useful for editors who want to toggle editing mode on-demand without permanently enabling it.

## Flash-all highlighting

The flash-all feature provides visual feedback by highlighting all editable elements on the page. This is useful for:
- Showing editors what content they can edit
- Debugging to verify Visual Editing is working correctly
- Onboarding new content editors

To trigger flash-all programmatically, use the `useContentLink` hook:

```jsx
import { useContentLink } from 'react-datocms';

function DebugButton() {
  const { flashAll } = useContentLink();

  return (
    <button onClick={() => flashAll(true)}>
      Show all editable areas
    </button>
  );
}
```

The `true` parameter scrolls to the nearest editable element, useful on long pages.

## Props

The `<ContentLink />` component accepts the following props:

| Prop                | Type                            | Default | Description                                                                                                                                                     |
| ------------------- | ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onNavigateTo`      | `(path: string) => void`        | -       | Callback when [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) requests navigation to a different page          |
| `currentPath`       | `string`                        | -       | Current pathname to sync with [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews)                                  |
| `enableClickToEdit` | `boolean \| ClickToEditOptions` | -       | Enable click-to-edit overlays on mount. Pass `true` or an object with options. If `false`/`undefined`, click-to-edit is disabled (use Alt/Option key to toggle) |
| `stripStega`        | `boolean`                       | -       | Whether to strip stega encoding from text nodes after stamping                                                                                                  |
| `root`              | `React.RefObject<HTMLElement>`  | -       | Ref to limit scanning to this root element instead of the entire document                                                                                       |
| `hue`               | `number`                        | `17`    | Hue (0–359) of the overlay accent color. Default is the DatoCMS hue (`17`). Use this to match your brand or project colors                                      |

## Advanced usage: the `useContentLink` hook

For more control over Visual Editing behavior, use the `useContentLink` hook directly. This is useful when you need to:
- Programmatically control click-to-edit mode
- Implement custom editing UIs
- React to editing state changes
- Integrate with custom frameworks or routing solutions

### Hook API

```typescript
import { useContentLink } from 'react-datocms';

const {
  controller,              // The underlying controller instance
  enableClickToEdit,       // Enable click-to-edit overlays
  disableClickToEdit,      // Disable click-to-edit overlays
  isClickToEditEnabled,    // Check if click-to-edit is enabled
  flashAll,                // Highlight all editable elements
  setCurrentPath,          // Notify Web Previews plugin of current path
} = useContentLink({
  // enabled can be:
  // - true (default): Enable with default settings (stega encoding preserved)
  // - false: Disable the controller
  // - { stripStega: true }: Enable and strip stega encoding for clean DOM
  enabled: true,
  onNavigateTo: (path) => { /* handle navigation */ },
  root: elementRef,
});
```

**Options:**

- `enabled?: boolean | { stripStega: boolean }` - Controls whether the controller is enabled and how it handles stega encoding:
  - `true` (default): Enables the controller with stega encoding preserved in the DOM (allows controller recreation)
  - `false`: Disables the controller completely
  - `{ stripStega: true }`: Enables the controller and permanently removes stega encoding from text nodes for clean `textContent` access
- `onNavigateTo?: (path: string) => void` - Callback when Web Previews plugin requests navigation
- `root?: React.RefObject<HTMLElement>` - Ref to limit scanning to this root element
- `hue?: number` - Hue (0–359) of the overlay accent color (default: `17`, the DatoCMS hue)

**Note:** The `<ContentLink />` component allows controlling stega stripping through the `stripStega` prop. When undefined, the underlying library's default behavior is used.

### Example: Custom editing toolbar

```jsx
import { useContentLink } from 'react-datocms';
import { useState } from 'react';

function EditingToolbar() {
  const { enableClickToEdit, disableClickToEdit, isClickToEditEnabled, flashAll } = useContentLink({
    onNavigateTo: (path) => window.location.href = path,
  });

  const [isEditing, setIsEditing] = useState(false);

  const toggleEditing = () => {
    if (isEditing) {
      disableClickToEdit();
    } else {
      enableClickToEdit({ scrollToNearestTarget: true });
    }
    setIsEditing(!isEditing);
  };

  return (
    <div className="editing-toolbar">
      <button onClick={toggleEditing}>
        {isEditing ? 'Disable' : 'Enable'} Editing
      </button>
      <button onClick={() => flashAll(true)}>
        Show Editable Areas
      </button>
    </div>
  );
}
```

### Example: Conditional editing in different environments

```jsx
import { useContentLink } from 'react-datocms';

function ConditionalEditing() {
  const isDraftMode = process.env.NEXT_PUBLIC_DRAFT_MODE === 'true';

  const { enableClickToEdit } = useContentLink({
    enabled: isDraftMode,
    onNavigateTo: (path) => router.push(path),
  });

  // Only enable in draft mode
  useEffect(() => {
    if (isDraftMode) {
      enableClickToEdit();
    }
  }, [isDraftMode, enableClickToEdit]);

  return null;
}
```

## Data attributes reference

This library uses several `data-datocms-*` attributes. Some are **developer-specified** (you add them to your markup), and some are **library-managed** (added automatically during DOM stamping). Here's a complete reference.

### Developer-specified attributes

These attributes are added by you in your templates/components to control how editable regions behave.

#### `data-datocms-content-link-url`

Manually marks an element as editable with an explicit edit URL. Use this for non-text fields (booleans, numbers, dates, JSON) that cannot contain stega encoding. The recommended approach is to use the `_editingUrl` field available on all records:

```graphql
query {
  product {
    id
    price
    isActive
    _editingUrl
  }
}
```

```tsx
<span data-datocms-content-link-url={product._editingUrl}>
  ${product.price}
</span>
```

#### `data-datocms-content-link-source`

Attaches stega-encoded metadata without the need to render it as content. Useful for structural elements that cannot contain text (like `<video>`, `<audio>`, `<iframe>`, etc.) or when stega encoding in visible text would be problematic:

```tsx
<div data-datocms-content-link-source={video.alt}>
  <video src={video.url} poster={video.posterImage.url} controls />
</div>
```

The value must be a stega-encoded string (any text field from the API will work). The library decodes the stega metadata from the attribute value and makes the element clickable to edit.

#### `data-datocms-content-link-group`

Expands the clickable area to a parent element. When the library encounters stega-encoded content, by default it makes the immediate parent of the text node clickable to edit. Adding this attribute to an ancestor makes that ancestor the clickable target instead:

```tsx
<article data-datocms-content-link-group>
  {/* product.title contains stega encoding */}
  <h2>{product.title}</h2>
  <p>${product.price}</p>
</article>
```

Here, clicking anywhere in the `<article>` opens the editor, rather than requiring users to click precisely on the `<h2>`.

**Important:** A group should contain only one stega-encoded source. If multiple stega strings resolve to the same group, the library logs a collision warning and only the last URL wins.

#### `data-datocms-content-link-boundary`

Stops the upward DOM traversal that looks for a `data-datocms-content-link-group`, making the element where stega was found the clickable target instead. This creates an independent editable region that won't merge into a parent group (see [How group and boundary resolution works](#how-group-and-boundary-resolution-works) below for details):

```tsx
<div data-datocms-content-link-group>
  {/* page.title contains stega encoding → resolves to URL A */}
  <h1>{page.title}</h1>
  <section data-datocms-content-link-boundary>
    {/* page.author contains stega encoding → resolves to URL B */}
    <span>{page.author}</span>
  </section>
</div>
```

Without the boundary, clicking `page.author` would open URL A (the outer group). With the boundary, the `<span>` becomes the clickable target opening URL B.

The boundary can also be placed directly on the element that contains the stega text:

```tsx
<div data-datocms-content-link-group>
  {/* page.title contains stega encoding → resolves to URL A */}
  <h1>{page.title}</h1>
  {/* page.author contains stega encoding → resolves to URL B */}
  <span data-datocms-content-link-boundary>{page.author}</span>
</div>
```

Here, the `<span>` has the boundary and directly contains the stega text, so the `<span>` itself becomes the clickable target (since the starting element and the boundary element are the same).

### Library-managed attributes

These attributes are added automatically by the library during DOM stamping. You do not need to add them yourself, but you can target them in CSS or JavaScript.

#### `data-datocms-contains-stega`

Added to elements whose text content contains stega-encoded invisible characters. This attribute is only present when `stripStega` is `false` (the default), since with `stripStega: true` the characters are removed entirely. Useful for CSS workarounds — the zero-width characters can sometimes cause unexpected letter-spacing or text overflow:

```css
[data-datocms-contains-stega] {
  letter-spacing: 0 !important;
}
```

#### `data-datocms-auto-content-link-url`

Added automatically to elements that the library has identified as editable targets (through stega decoding and group/boundary resolution). Contains the resolved edit URL.

This is the automatic counterpart to the developer-specified `data-datocms-content-link-url`. The library adds `data-datocms-auto-content-link-url` wherever it can extract an edit URL from stega encoding, while `data-datocms-content-link-url` is needed for non-text fields (booleans, numbers, dates, etc.) where stega encoding cannot be embedded. Both attributes are used by the click-to-edit overlay system to determine which elements are clickable and where they link to.

## How group and boundary resolution works

When the library encounters stega-encoded content inside an element, it walks up the DOM tree from that element:

1. If it finds a `data-datocms-content-link-group`, it stops and stamps **that** element as the clickable target.
2. If it finds a `data-datocms-content-link-boundary`, it stops and stamps the **starting element** as the clickable target — further traversal is prevented.
3. If it reaches the root without finding either, it stamps the **starting element**.

Here are some concrete examples to illustrate:

**Example 1: Nested groups**

```tsx
<div data-datocms-content-link-group>
  {/* page.title contains stega encoding → resolves to URL A */}
  <h1>{page.title}</h1>
  <div data-datocms-content-link-group>
    {/* page.subtitle contains stega encoding → resolves to URL B */}
    <p>{page.subtitle}</p>
  </div>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.subtitle`**: walks up from `<p>`, finds the inner group first → the **inner `<div>`** becomes clickable (opens URL B). The outer group is never reached.

Each nested group creates an independent clickable region. The innermost group always wins for its own content.

**Example 2: Boundary preventing group propagation**

```tsx
<div data-datocms-content-link-group>
  {/* page.title contains stega encoding → resolves to URL A */}
  <h1>{page.title}</h1>
  <section data-datocms-content-link-boundary>
    {/* page.author contains stega encoding → resolves to URL B */}
    <span>{page.author}</span>
  </section>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.author`**: walks up from `<span>`, hits the `<section>` boundary → traversal stops, the **`<span>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 3: Boundary inside a group**

```tsx
<div data-datocms-content-link-group>
  {/* page.description contains stega encoding → resolves to URL A */}
  <p>{page.description}</p>
  <div data-datocms-content-link-boundary>
    {/* page.footnote contains stega encoding → resolves to URL B */}
    <p>{page.footnote}</p>
  </div>
</div>
```

- **`page.description`**: walks up from `<p>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.footnote`**: walks up from `<p>`, hits the boundary → traversal stops, the **`<p>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 4: Multiple stega strings without groups (collision warning)**

```tsx
<p>
  {/* Both product.name and product.tagline contain stega encoding */}
  {product.name}
  {product.tagline}
</p>
```

Both stega-encoded strings resolve to the same `<p>` element. The library logs a console warning and the last URL wins. To fix this, wrap each piece of content in its own element:

```tsx
<p>
  <span>{product.name}</span>
  <span>{product.tagline}</span>
</p>
```

## Structured Text fields

Structured Text fields require special attention because of how stega encoding works within them:

- The DatoCMS API encodes stega information inside a single `<span>` within the structured text output. Without any configuration, only that small span would be clickable.
- Structured Text fields can contain **embedded blocks** and **inline records**, each with their own editing URL that should open a different record in the editor.

Here are the rules to follow:

### Rule 1: Always wrap the Structured Text component in a group

This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```tsx
<div data-datocms-content-link-group>
  <StructuredText data={page.content} />
</div>
```

### Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary

Embedded blocks, inline records, and inline blocks have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Add `data-datocms-content-link-boundary` to prevent them from merging into the parent group:

```tsx
<div data-datocms-content-link-group>
  <StructuredText
    data={page.content}
    renderBlock={({ record }) => (
      <div data-datocms-content-link-boundary>
        <BlockComponent block={record} />
      </div>
    )}
    renderInlineRecord={({ record }) => (
      <span data-datocms-content-link-boundary>
        <InlineRecordComponent record={record} />
      </span>
    )}
    renderLinkToRecord={({ record, children, transformedMeta }) => (
      <a {...transformedMeta} href={`/resources/${record.slug}`}>
        {children}
      </a>
    )}
    renderInlineBlock={({ record }) => (
      <span data-datocms-content-link-boundary>
        <InlineBlockComponent record={record} />
      </span>
    )}
  />
</div>
```

With this setup:
- Clicking the main text (paragraphs, headings, lists) opens the **structured text field editor**
- Clicking an embedded block, inline record, or inline block opens **that record's editor**

**Why `renderLinkToRecord` doesn't need a boundary:** Record links are typicall just `<a>` tags wrapping text that already belongs to the surrounding structured text. Since they don't introduce a separate editing target, there's no URL collision and no reason to isolate them from the parent group.

## Low-level utilities

The `react-datocms/stega` subpath re-exports utility functions from `@datocms/content-link` for working with stega-encoded content. These are pure, server-safe functions, so they can be used in both React Server Components and client components:

```typescript
import { stripStega, decodeStega, revealStega } from 'react-datocms/stega';
```

### `decodeStega`

Decodes stega-encoded content to extract editing metadata:

```typescript
import { decodeStega } from 'react-datocms/stega';

const text = "Hello, world!"; // Contains invisible stega data
const decoded = decodeStega(text);

if (decoded) {
  console.log('Editing URL:', decoded.url);
  console.log('Clean text:', decoded.cleanText);
}
```

### `stripStega`

Removes stega encoding from any data type (strings, objects, arrays, primitives):

```typescript
import { stripStega } from 'react-datocms/stega';

// Works with strings
stripStega("Hello‎World") // "HelloWorld"

// Works with objects
stripStega({ name: "John‎", age: 30 })

// Works with nested structures - removes ALL stega encodings
stripStega({
  users: [
    { name: "Alice‎", email: "alice‎.com" },
    { name: "Bob‎", email: "bob‎.co" }
  ]
})

// Works with arrays
stripStega(["First‎", "Second‎", "Third‎"])
```

### `revealStega`

Like `stripStega`, but instead of silently removing the invisible characters it replaces each occurrence with a human-readable `[STEGA:/editor/...]` tag — useful for debugging or logging what stega encoding is actually present in a value:

```typescript
import { revealStega } from 'react-datocms/stega';

revealStega("Hello‎world")
// "Hello[STEGA:/editor/item_types/123/items/456]world"

// Works on entire GraphQL responses
revealStega(graphqlResponse)
```

These utilities are useful when you need to:
- Extract clean text for meta tags or social sharing
- Check if content has stega encoding
- Debug Visual Editing issues
- Process stega-encoded content programmatically

## Troubleshooting

### Click-to-edit overlays not appearing

**Problem**: Overlays don't appear when clicking on content.

**Solutions**:
1. Verify stega encoding is enabled in your API calls:
   ```js
   const result = await executeQuery(query, {
     token: 'YOUR_API_TOKEN',
     contentLink: 'v1',
     baseEditingUrl: 'https://your-project.admin.datocms.com',
   });
   ```

2. Check that `<ContentLink />` is mounted in your component tree

3. Ensure you've enabled click-to-edit mode:
   ```jsx
   <ContentLink enableClickToEdit={true} />
   ```
   Or hold Alt/Option key while browsing

4. Check browser console for errors

### Navigation not syncing with [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews)

**Problem**: When you navigate in your preview, the DatoCMS editor doesn't follow along.

**Solutions**:
1. Ensure you're providing both `onNavigateTo` and `currentPath` props:
   ```jsx
   <ContentLink
     onNavigateTo={(path) => router.push(path)}
     currentPath={pathname}
   />
   ```

2. Verify `currentPath` updates when navigation occurs

3. Check that `baseEditingUrl` in your API calls matches your preview URL

### StructuredText blocks not clickable

**Problem**: Content within StructuredText blocks doesn't have click-to-edit overlays.

**Solutions**:
1. Wrap StructuredText with `data-datocms-content-link-group` (see [Rule 1](#rule-1-always-wrap-the-structured-text-component-in-a-group)):
   ```jsx
   <div data-datocms-content-link-group>
     <StructuredText data={content} />
   </div>
   ```

2. Add `data-datocms-content-link-boundary` to custom blocks and inline blocks (see [Rule 2](#rule-2-wrap-embedded-blocks-and-inline-records-in-a-boundary)):
   ```jsx
   renderBlock={({ record }) => (
     <div data-datocms-content-link-boundary>
       <CustomBlock record={record} />
     </div>
   )}
   renderInlineBlock={({ record }) => (
     <span data-datocms-content-link-boundary>
       <CustomInlineBlock record={record} />
     </span>
   )}
   ```

### Layout issues caused by stega encoding

**Problem**: The invisible zero-width characters can cause unexpected letter-spacing or text breaking out of containers.

**Solutions**:
1. Use the `stripStega` prop to remove stega encoding after processing:
   ```jsx
   <ContentLink stripStega={true} />
   ```

2. Or use CSS to fix the letter-spacing issue:
   ```css
   [data-datocms-contains-stega] {
     letter-spacing: 0 !important;
   }
   ```
   This attribute is automatically added to elements with stega-encoded content when `stripStega` is `false` (the default).

---

# vue-datocms — Vue components and composables for DatoCMS

Source [github]: https://raw.githubusercontent.com/datocms/vue-datocms/master/README.md

[![MIT](https://img.shields.io/npm/l/vue-datocms?style=for-the-badge)](https://github.com/datocms/vue-datocms/blob/master/LICENSE) [![NPM](https://img.shields.io/npm/v/vue-datocms?style=for-the-badge)](https://www.npmjs.com/package/vue-datocms) [![Build Status](https://img.shields.io/github/actions/workflow/status/datocms/vue-datocms/node.js.yml?branch=master&style=for-the-badge)](https://github.com/datocms/vue-datocms/actions/workflows/node.js.yml)

A set of components and utilities to work faster with [DatoCMS](https://www.datocms.com/) in Vue.js environments. Integrates seamlessly with [DatoCMS's GraphQL Content Delivery API](https://www.datocms.com/docs/content-delivery-api).

- Works with Vue 3 (version 4 is maintained for compatibility with Vue 2);
- TypeScript ready;
- Compatible with any data-fetching library (axios, Apollo);
- Usable both client and server side;
- Compatible with vanilla Vue and pretty much any other Vue-based solution.

## Table of Contents

- [vue-datocms](#vue-datocms)
  - [Table of Contents](#table-of-contents)
  - [Features](#features)
  - [Installation](#installation)
  - [Development](#development)
  - [Releasing (maintainers)](#releasing-maintainers)
  - [Trying a change before it's released](#trying-a-change-before-its-released)
- [What is DatoCMS?](#what-is-datocms)

## Features

`vue-datocms` contains Vue components ready to use, helpers functions and usage examples.

[Components](https://vuejs.org/guide/essentials/component-basics.html):

- [`<ContentLink />`](src/components/ContentLink) for Visual Editing with click-to-edit overlays
- [`<Image />` and `<NakedImage />`](src/components/Image)
- [`<VideoPlayer />`](src/components/VideoPlayer)
- [`<StructuredText />`](src/components/StructuredText)

[Composables](https://vuejs.org/guide/reusability/composables.html):

- [`useContentLink`](src/composables/useContentLink) for Visual Editing
- [`useQuerySubscription`](src/composables/useQuerySubscription)
- [`useSiteSearch`](src/composables/useSiteSearch)
- [`useVideoPlayer`](src/composables/useVideoPlayer)

Helpers:

- [`toHead`](src/lib/toHead)

## Installation

```
# First, install Vue
npm install vue
# Then install vue-datocms
npm install vue-datocms

# Demos

For fully working examples take a look at our [examples directory](https://github.com/datocms/vue-datocms/tree/master/examples).

Live demo: [https://vue-datocms-example.netlify.com/](https://vue-datocms-example.netlify.com/)

```
## Development

This repository contains a number of demos/examples. You can use them to locally test your changes.

```bash
cd examples
npm setup
npm run dev
```

## Releasing (maintainers)

Every user-visible change needs a changeset: run `npx changeset` from the repo
root in the same PR, pick the bump level (`patch` is for bug fixes only, new API
surface is `minor`) and commit the file it writes under `.changeset/`. That is
where the changelog entry comes from, and it is where the bump level is decided
— not on release day. See [`.changeset/README.md`](.changeset/README.md).

To release, from an up-to-date, clean `master`, run `npm run release`. It
builds and tests, applies the pending changesets — bumping the version and
writing `CHANGELOG.md` — publishes to npm, and only then tags `vX.Y.Z`, pushes,
and creates a GitHub release whose notes come straight from that changelog
entry. An interrupted release is resumed by re-running it, never undone. Use
`npm run release:next` for a prerelease under the `next` dist-tag.

The script is
[`@datocms/release-toolchain`](https://github.com/datocms/release-toolchain),
shared with every other DatoCMS repository and pinned here by tag.

---

# Vue/Nuxt — Responsive <datocms-image> and <datocms-naked-image> components

Source [github]: https://raw.githubusercontent.com/datocms/vue-datocms/master/src/components/Image/README.md

## Progressive/responsive images

`<datocms-image>` and `<datocms-naked-image>` are Vue components specially designed to work seamlessly with DatoCMS’s [`responsiveImage` GraphQL query](https://www.datocms.com/docs/content-delivery-api/uploads#responsive-images) which optimizes image loading for your websites.

- TypeScript ready;
- Usable both client and server side;
- Compatible with vanilla Vue, Nuxt and pretty much any other Vue-based solution;

### Out-of-the-box features

- Offers optimized version of images for browsers that support WebP/AVIF format
- Generates multiple smaller images so smartphones and tablets don’t download desktop-sized images
- Efficiently lazy loads images to speed initial page load and save bandwidth
- Holds the image position so your page doesn’t jump while images load
- Uses either blur-up or background color techniques to show a preview of the image while it loads

## Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Setup](#setup)
- [`<datocms-image />` vs `<datocms-naked-image />`](#datocms-image--vs-datocms-naked-image-)
- [Usage](#usage)
- [Example](#example)
- [The `ResponsiveImage` object](#the-responsiveimage-object)
- [`<datocms-naked-image>`](#datocms-naked-image)
  - [Props](#props)
  - [Exposed public properties](#exposed-public-properties)
  - [Events](#events)
- [`<datocms-image>`](#datocms-image)
  - [Props](#props-1)
  - [Events](#events-1)
  - [Exposed public properties](#exposed-public-properties-1)
  - [Layout mode](#layout-mode)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->


## Setup

You can register the components globally so they are available in your app:

```js
import Vue from 'vue';
import { DatocmsImagePlugin, DatocmsNakedImagePlugin } from 'vue-datocms';

Vue.use(DatocmsImagePlugin);
Vue.use(DatocmsNakedImagePlugin);
```

Or use it locally in any of your components:

```js
import { Image, NakedImage } from 'vue-datocms';

export default {
  components: {
    'datocms-image': Image,
    'datocms-naked-image': NakedImage,
  },
};
```

## `<datocms-image />` vs `<datocms-naked-image />`

Even though their purpose is the same, there are some significant differences between these two components. Depending on your specific needs, you can choose to use one or the other:

* `<datocms-naked-image />` generates minimum JS footprint, outputs a single `<picture />` element and implements lazy-loading using the native [`loading="lazy"` attribute](https://web.dev/articles/browser-level-image-lazy-loading). The placeholder is set as the background to the image itself.
* `<datocms-image />` has the ability to set a cross-fade effect between the placeholder and the original image, but at the cost of generating more complex HTML output composed of multiple elements around the main `<picture />` element. It also implements lazy-loading through `IntersectionObserver`, which allows customization of the thresholds at which lazy loading occurs.


## Usage

1. Use `<datocms-image>` or `<datocms-naked-image>` it in place of the regular `<img />` tag
2. Write a GraphQL query to your DatoCMS project using the [`responsiveImage` query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#responsive-images)

The GraphQL query returns multiple thumbnails with optimized compression. The `<datocms-image>` component automatically sets up the "blur-up" effect as well as lazy loading of images further down the screen.

## Example

For a fully working example take a look at our [examples directory](https://github.com/datocms/vue-datocms/tree/master/examples).

```vue
<template>
  <article>
    <div v-if="data">
      <h1>{{ data.blogPost.title }}</h1>
      <datocms-image :data="data.blogPost.cover.responsiveImage" />
      <datocms-naked-image :data="data.blogPost.cover.responsiveImage" />
    </div>
  </article>
</template>

<script>
import { request } from './lib/datocms';
import { Image, NakedImage } from 'vue-datocms';

const query = gql`
  query {
    blogPost {
      title
      cover {
        responsiveImage(
          imgixParams: { fit: crop, w: 300, h: 300, auto: format }
        ) {
          # always required
          src
          width
          height
          # not required, but strongly suggested!
          alt
          title
          # blur-up placeholder, JPEG format, base64-encoded, or...
          base64
          # background color placeholder
          bgColor
          # you can omit `sizes` if you explicitly pass the `sizes` prop to the image component
          sizes
        }
      }
    }
  }
`;

export default {
  components: {
    'datocms-image': Image,
    'datocms-naked-image': NakedImage,
  },
  data() {
    return {
      data: null,
    };
  },
  async mounted() {
    this.data = await request({ query });
  },
};
</script>
```

## The `ResponsiveImage` object

The `data` prop of both components expects an object with the same shape as the one returned by `responsiveImage` GraphQL call. It's up to you to make a GraphQL query that will return the properties you need for a specific use of the `<datocms-image>` component.

- The minimum required properties for `data` are: `src`, `width` and `height`;
- `alt` and `title`, while not mandatory, are all highly suggested, so remember to use them!
- If you don't request `srcSet`, the component will auto-generate an `srcset` based on `src` + the `srcSetCandidates` prop (it can help reducing the GraphQL response size drammatically when many images are returned);
- We strongly to suggest to always specify [`{ auto: format }`](https://docs.imgix.com/apis/rendering/auto/auto#format) in your `imgixParams`, instead of requesting `webpSrcSet`, so that you can also take advantage of more performant optimizations (AVIF), without increasing GraphQL response size;
- If you request both the `bgColor` and `base64` property, the latter will take precedence, so just avoid querying both fields at the same time, as it will only make the GraphQL response bigger :wink:;
- You can avoid requesting `sizes` and directly pass a `sizes` prop to the component to reduce the GraphQL response size;
Here's a complete recap of what `responsiveImage` offers:

| property    | type    | required           | description                                                                                                                                                                                    |
| ----------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| src         | string  | :white_check_mark: | The `src` attribute for the image                                                                                                                                                              |
| width       | integer | :white_check_mark: | The width of the image                                                                                                                                                                         |
| height      | integer | :white_check_mark: | The height of the image                                                                                                                                                                        |
| alt         | string  | :x:                | Alternate text (`alt`) for the image (not required, but strongly suggested!)                                                                                                                   |
| title       | string  | :x:                | Title attribute (`title`) for the image (not required, but strongly suggested!)                                                                                                                |
| sizes       | string  | :x:                | The HTML5 `sizes` attribute for the image (omit it if you're already passing a `sizes` prop to the Image component)                                                                            |
| base64      | string  | :x:                | A base64-encoded thumbnail to offer during image loading                                                                                                                                       |
| bgColor     | string  | :x:                | The background color for the image placeholder (omit it if you're already requesting `base64`)                                                                                                 |
| srcSet      | string  | :x:                | The HTML5 `srcSet` attribute for the image (can be omitted, the Image component knows how to build it based on `src`)                                                                          |
| webpSrcSet  | string  | :x:                | The HTML5 `srcSet` attribute for the image in WebP format (deprecated, it's better to use the [`auto=format`](https://docs.imgix.com/apis/rendering/auto/auto#format) Imgix transform instead) |
| aspectRatio | float   | :x:                | The aspect ratio (width/height) of the image                                                                                                                                                   |


## `<datocms-naked-image>`

### Props

| prop               | type                     | default                            | required           | description                                                                                                                                          |
| ------------------ | ------------------------ | ---------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| data               | `ResponsiveImage` object |                                    | :white_check_mark: | The actual response you get from a DatoCMS `responsiveImage` GraphQL query                            ****                                           |
| picture-class      | string                   | null                               | :x:                | Additional CSS class for the root `<picture>` tag                                                                                                    |
| picture-style      | CSS properties           | null                               | :x:                | Additional CSS rules to add to the root `<picture>` tag                                                                                              |
| img-class          | string                   | null                               | :x:                | Additional CSS class for the `<img>` tag                                                                                                             |
| img-style          | CSS properties           | null                               | :x:                | Additional CSS rules to add to the `<img>` tag                                                                                                       |
| priority           | Boolean                  | false                              | :x:                | Disables lazy loading, and sets the image [fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) to "high" |
| sizes              | string                   | undefined                          | :x:                | The HTML5 [`sizes`](https://web.dev/learn/design/responsive-images/#sizes) attribute for the image (will be used `data.sizes` as a fallback)         |
| use-placeholder    | Boolean                  | true                               | :x:                | Whether the image should use a blurred image placeholder                                                                                             |
| src-set-candidates | Array<number>            | [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4] | :x:                | If `data` does not contain `srcSet`, the candidates for the `srcset` attribute of the image will be auto-generated based on these width multipliers  |
| referrer-policy    | string                   | `no-referrer-when-downgrade`       | :x:                | Defines which referrer is sent when fetching the image. Defaults to `no-referrer-when-downgrade` to give more useful stats in DatoCMS Project Usages |

### Exposed public properties

| prop     | type               | description             |
| -------- | ------------------ | ----------------------- |
| imageRef | `HTMLImageElement` | `ref()` to the img node |

### Events

| prop  | description                                 |
| ----- | ------------------------------------------- |
| @load | Emitted when the image has finished loading |

## `<datocms-image>`

### Props

| prop                   | type                                             | required                     | description                                                                                                                                                                                                                                                                                   | default                                                                                                                                              |
| ---------------------- | ------------------------------------------------ | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| data                   | `ResponsiveImage` object                         | :white_check_mark:           | The actual response you get from a DatoCMS `responsiveImage` GraphQL query                                                                                                                                                                                                                    |                                                                                                                                                      |
| layout                 | 'intrinsic' \| 'fixed' \| 'responsive' \| 'fill' | :x:                          | The layout behavior of the image as the viewport changes size                                                                                                                                                                                                                                 | "intrinsic"                                                                                                                                          |
| fade-in-duration       | integer                                          | :x:                          | Duration (in ms) of the fade-in transition effect upon image loading                                                                                                                                                                                                                          | 500                                                                                                                                                  |
| intersection-threshold | float                                            | :x:                          | Indicate at what percentage of the placeholder visibility the loading of the image should be triggered. A value of 0 means that as soon as even one pixel is visible, the callback will be run. A value of 1.0 means that the threshold isn't considered passed until every pixel is visible. | 0                                                                                                                                                    |
| intersection-margin    | string                                           | :x:                          | Margin around the placeholder. Can have values similar to the CSS margin property (top, right, bottom, left). The values can be percentages. This set of values serves to grow or shrink each side of the placeholder element's bounding box before computing intersections.                  | "0px 0px 0px 0px"                                                                                                                                    |
| priority               | Boolean                                          | :x:                          | Disables lazy loading, and sets the image [fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) to "high"                                                                                                                                          | false                                                                                                                                                |
| sizes                  | string                                           | :x:                          | The HTML5 [`sizes`](https://web.dev/learn/design/responsive-images/#sizes) attribute for the image (will be used `data.sizes` as a fallback)                                                                                                                                                  | undefined                                                                                                                                            |
| use-placeholder        | Boolean                                          | :x:                          | Whether the component should use a blurred image placeholder                                                                                                                                                                                                                                  | true                                                                                                                                                 |
| src-set-candidates     | Array<number>                                    | :x:                          | If `data` does not contain `srcSet`, the candidates for the `srcset` attribute of the image will be auto-generated based on these width multipliers                                                                                                                                           | [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4]                                                                                                                   |
| class                  | string                                           | :x:                          | Additional CSS className for root node                                                                                                                                                                                                                                                        | null                                                                                                                                                 |
| style                  | CSS properties                                   | :x:                          | Additional CSS rules to add to the root node                                                                                                                                                                                                                                                  | null                                                                                                                                                 |
| picture-class          | string                                           | :x:                          | Additional CSS class for the inner `<picture />` tag                                                                                                                                                                                                                                          | null                                                                                                                                                 |
| picture-style          | CSS properties                                   | :x:                          | Additional CSS rules to add to the inner `<picture />` tag                                                                                                                                                                                                                                    | null                                                                                                                                                 |
| img-class              | string                                           | :x:                          | Additional CSS class for the image inside the `<picture />` tag                                                                                                                                                                                                                               | null                                                                                                                                                 |
| img-style              | CSS properties                                   | :x:                          | Additional CSS rules to add to the image inside the `<picture />` tag                                                                                                                                                                                                                         | null                                                                                                                                                 |
| placeholder-class      | string                                           | :x:                          | Additional CSS class for the placeholder image                                                                                                                                                                                                                                                | null                                                                                                                                                 |
| placeholder-style      | CSS properties                                   | :x:                          | Additional CSS rules for the placeholder image                                                                                                                                                                                                                                                | null                                                                                                                                                 |
| referrer-policy        | string                                           | `no-referrer-when-downgrade` | :x:                                                                                                                                                                                                                                                                                           | Defines which referrer is sent when fetching the image. Defaults to `no-referrer-when-downgrade` to give more useful stats in DatoCMS Project Usages |

### Events

| prop  | description                                 |
| ----- | ------------------------------------------- |
| @load | Emitted when the image has finished loading |


### Exposed public properties

| prop     | type               | description              |
| -------- | ------------------ | ------------------------ |
| rootRef  | `HTMLDivElement`   | `ref()` to the root node |
| imageRef | `HTMLImageElement` | `ref()` to the img node  |


### Layout mode

With the `layout` property, you can configure the behavior of the image as the viewport changes size:

- When `intrinsic`, the image will scale the dimensions down for smaller viewports, but maintain the original dimensions for larger viewports.
- When `fixed`, the image dimensions will not change as the viewport changes (no responsiveness) similar to the native `img` element.
- When `responsive` (default behaviour), the image will scale the dimensions down for smaller viewports and scale up for larger viewports.
- When `fill`, the image will stretch both width and height to the dimensions of the parent element, provided the parent element is relative.
  - This is usually paired with the `objectFit` and `objectPosition` properties.
  - Ensure the parent element has `position: relative` in their stylesheet.

---

# Vue/Nuxt — <VideoPlayer> component for Mux-encoded videos

Source [github]: https://raw.githubusercontent.com/datocms/vue-datocms/master/src/components/VideoPlayer/README.md

`<VideoPlayer />` is a Vue component specially designed to work seamlessly with
DatoCMS’s [`video` GraphQL query][q]) that optimizes video streaming for your
sites.

[q]: https://www.datocms.com/docs/content-delivery-api/images-and-videos#videos

To stream videos, DatoCMS partners with MUX, a video CDN that serves optimized
streams to your users. Our component is a wrapper around 
[MUX's video player][mvp] [web component][wc]. It takes care of the details for you, and this
is our recommended way to serve optimal videos to your users.

[mvp]: https://github.com/muxinc/elements/blob/main/packages/mux-player/README.md
[wc]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components

## Out-of-the-box features

- Offers optimized streaming so smartphones and tablets don’t request desktop-sized videos
- Lazy loads the underlying video player web component and the video to be
  played to speed initial page load and save bandwidth
- Holds the video position so your page doesn’t jump while the player loads
- Uses blur-up technique to show a placeholder of the video while it loads

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**

- [Installation](#installation)
  - [Setup](#setup)
- [Usage](#usage)
- [Props](#props)
- [Opt-in Viewer Analytics](#opt-in-viewer-analytics)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->


## Installation

```sh
npm install --save vue-datocms @mux/mux-player
```

`@mux/mux-player` is a [peer dependency][pd] for `vue-datocms`: so you're
expected to add it to your project.

[pd]: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependencies

### Setup

You can register the component globally so it's available in all your apps:

```js
import Vue from 'vue';
import { DatocmsVideoPlayerPlugin } from 'vue-datocms';

Vue.use(DatocmsVideoPlayerPlugin);
```

Or use it locally in any of your components:

```js
import { VideoPlayer } from 'vue-datocms';

export default {
  components: {
    'datocms-video-player': VideoPlayer,
  },
};
```

## Usage

```vue
<template>
  <article>
    <div v-if="data">
      <h1>{{ data.blogPost.title }}</h1>
      <datocms-video-player :data="data.blogPost.video" />
    </div>
  </article>
</template>

<script>
import { request } from './lib/datocms';
import { VideoPlayer } from 'vue-datocms';

// The GraphQL query returns data that the `VideoPlayer` component
// automatically uses to properly size the player, set up a “blur-up”
// placeholder as well as lazy loading the video.
const query = gql`
  query {
    blogPost {
      title
      cover {
        video {
          # required: this field identifies the video to be played
          muxPlaybackId

          # all the other fields are not required but:

          # if provided, title is displayed in the upper left corner of the video
          title

          # if provided, width and height are used to define the aspect ratio of the
          # player, so to avoid layout jumps during the rendering.
          width
          height

          # if provided, it shows a blurred placeholder for the video
          blurUpThumb

          # if provided, it enables DatoCMS Content Link for click-to-edit overlays
          alt

          # you can include more data here: they will be ignored by the component
        }
      }
    }
  }
`;

export default {
  components: {
    'datocms-video-player': VideoPlayer,
  },
  data() {
    return {
      data: null,
    };
  },
  async mounted() {
    this.data = await request({ query });
  },
};
</script>
```

## Props

The `<VideoPlayer />` component supports as props all the
[attributes][attributes] of the `<mux-player />` web component, plus `data`,
which is meant to receive data directly in the shape they are provided by
DatoCMS GraphQL API.

[attributes]: https://github.com/muxinc/elements/blob/main/packages/mux-player/REFERENCE.md

`<VideoPlayer />` uses the `data` prop to generate a set of attributes for the
inner `<mux-player />`.

| prop | type           | required           | description                                                      | default |
| ---- | -------------- | ------------------ | ---------------------------------------------------------------- | ------- |
| data | `Video` object | :white_check_mark: | The actual response you get from a DatoCMS `video` GraphQL query |         |

`<VideoPlayer />` generate some default attributes:

- when not declared, the `disable-cookies` prop is true, unless you explicitly
  set the prop to `false` (therefore it generates a `disable-cookies` attribute)
- when not declared, the `preload` prop defaults to `metadata`, for an optimal UX experience together with saved bandwidth
- the video height and width, when available in the `data` props, are used to
  set a default `aspect-ratio: [width] / [height];` for the `<mux-player />`'s
  `style` attribute

All the other props are forwarded to the `<mux-player />` web component that is used internally.

## Opt-in Viewer Analytics

This `<VideoPlayer/>` component can OPTIONALLY collect clientside [playback and engagement metrics](https://www.mux.com/data#TechSpecs) such as playback percentages, user agents, and geography.

These analytics are **disabled** by default. To enable them, you must opt in to [Mux Data](https://www.mux.com/data) integration by creating a Mux Data account (free) and providing its `envKey` to the component.

For details and setup instructions, please see our documentation on **[Streaming Video Analytics with Mux Data](https://www.datocms.com/docs/streaming-videos/streaming-video-analytics-with-mux-data)**.

---

# Vue/Nuxt — <datocms-structured-text> component to render Structured Text fields

Source [github]: https://raw.githubusercontent.com/datocms/vue-datocms/master/src/components/StructuredText/README.md

`<datocms-structured-text />` is a Vue component that you can use to render the value contained inside a DatoCMS [Structured Text field type](https://www.datocms.com/docs/structured-text/dast).

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Setup](#setup)
- [Basic usage](#basic-usage)
- [Custom renderers](#custom-renderers)
- [Override default rendering of nodes](#override-default-rendering-of-nodes)
- [Props](#props)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Setup

You can register the component globally so it's available in all your apps:

```js
import Vue from 'vue';
import { DatocmsStructuredTextPlugin } from 'vue-datocms';

Vue.use(DatocmsStructuredTextPlugin);
```

Or use it locally in any of your components:

```js
import { StructuredText } from 'vue-datocms';

export default {
  components: {
    'datocms-structured-text': StructuredText,
  },
};
```

## Basic usage

```vue
<template>
  <article>
    <div v-if="data">
      <h1>{{ data.blogPost.title }}</h1>
      <datocms-structured-text :data="data.blogPost.content" />
      <!--
        Final result:
        <h1>Hello <strong>world!</strong></h1>
      -->
    </div>
  </article>
</template>

<script>
import { request } from './lib/datocms';
import { StructuredText } from 'vue-datocms';

const query = gql`
  query {
    blogPost {
      title
      content {
        value
      }
    }
  }
`;

export default {
  components: {
    'datocms-structured-text': StructuredText,
  },
  data() {
    return {
      data: null,
    };
  },
  async mounted() {
    this.data = await request({ query });
    // data.blogPost.content ->
    // {
    //   value: {
    //     schema: "dast",
    //     document: {
    //       type: "root",
    //       children: [
    //         {
    //           type: "heading",
    //           level: 1,
    //           children: [
    //             {
    //               type: "span",
    //               value: "Hello ",
    //             },
    //             {
    //               type: "span",
    //               marks: ["strong"],
    //               value: "world!",
    //             },
    //           ],
    //         },
    //       ],
    //     },
    //   },
    // }
  },
};
</script>
```

## Custom renderers

You can also pass custom renderers for special nodes (inline records, record links and blocks) as an optional parameter like so:

```vue
<template>
  <article>
    <div v-if="data">
      <h1>{{ data.blogPost.title }}</h1>
      <datocms-structured-text
        :data="data.blogPost.content"
        :renderInlineRecord="renderInlineRecord"
        :renderLinkToRecord="renderLinkToRecord"
        :renderBlock="renderBlock"
      />
      <!--
        Final result:

        <h1>Welcome onboard <a href="/team/mark-smith">Mark</a></h1>
        <p>
          So happy to have
          <a href="/team/mark-smith">this awesome humang being</a> in our team!
        </p>
        <img
          src="https://www.datocms-assets.com/205/1597757278-austin-distel-wd1lrb9oeeo-unsplash.jpg"
          alt="Our team at work"
        />
      -->
    </div>
  </article>
</template>

<script>
import { request } from './lib/datocms';
import { StructuredText, Image } from 'vue-datocms';
import { h } from 'vue';

const query = gql`
  query {
    blogPost {
      title
      content {
        value
        links {
          ... on RecordInterface {
            __typename
            id
          }
          ... on TeamMemberRecord {
            firstName
            slug
          }
        }
        blocks {
          ... on RecordInterface {
            __typename
            id
          }
          ... on ImageRecord {
            image {
              responsiveImage(
                imgixParams: { fit: crop, w: 300, h: 300, auto: format }
              ) {
                srcSet
                webpSrcSet
                sizes
                src
                width
                height
                aspectRatio
                alt
                title
                base64
              }
            }
          }
        }
        inlineBlocks {
          ... on RecordInterface {
            __typename
            id
          }
          ... on MentionRecord {
            username
          }
        }
      }
    }
  }
`;

export default {
  components: {
    'datocms-structured-text': StructuredText,
    'datocms-image': Image,
  },
  data() {
    return {
      data: null,
    };
  },
  methods: {
    renderInlineRecord: ({ record }) => {
      switch (record.__typename) {
        case 'TeamMemberRecord':
          return h('a', { href: `/team/${record.slug}` }, record.firstName);
        default:
          return null;
      }
    },
    renderLinkToRecord: ({ record, children, transformedMeta }) => {
      switch (record.__typename) {
        case 'TeamMemberRecord':
          return h(
            'a',
            { ...transformedMeta, href: `/team/${record.slug}` },
            children,
          );
        default:
          return null;
      }
    },
    renderBlock: ({ record }) => {
      switch (record.__typename) {
        case 'ImageRecord':
          return h('datocms-image', {
            data: record.image.responsiveImage,
          });
        default:
          return null;
      }
    },
    renderInlineBlock: ({ record }) => {
      switch (record.__typename) {
        case 'MentionRecord':
          return h('code', `@${record.username}`);
        default:
          return null;
      }
    },
  },
  async mounted() {
    this.data = await request({ query });
    // data.blogPost.content ->
    // {
    //   value: {
    //     schema: "dast",
    //     document: {
    //       type: "root",
    //       children: [
    //         {
    //           type: "heading",
    //           level: 1,
    //           children: [
    //             { type: "span", value: "Welcome onboard " },
    //             { type: "inlineItem", item: "324321" },
    //           ],
    //         },
    //         {
    //           type: "paragraph",
    //           children: [
    //             { type: "span", value: "So happy to have " },
    //             {
    //               type: "itemLink",
    //               item: "324321",
    //               children: [
    //                 {
    //                   type: "span",
    //                   marks: ["strong"],
    //                   value: "this awesome humang being",
    //                 },
    //               ]
    //             },
    //             { type: "span", value: " in our team! We call him" },
    //             { type: "inlineBlock", item: "1984560" },
    //           ]
    //         },
    //         { type: "block", item: "1984559" }
    //       ],
    //     },
    //   },
    //   links: [
    //     {
    //       id: "324321",
    //       __typename: "TeamMemberRecord",
    //       firstName: "Mark",
    //       slug: "mark-smith",
    //     },
    //   ],
    //   blocks: [
    //     {
    //       id: "1984559",
    //       __typename: "ImageRecord",
    //       image: {
    //         responsiveImage: { ... },
    //       },
    //     },
    //   ],
    //   inlineBlocks: [
    //     {
    //       id: "1984560",
    //       __typename: "MentionRecord",
    //       username: "steffoz"
    //     },
    //   ],
    // }
  },
};
</script>
```

## Override default rendering of nodes

This component automatically renders all nodes except for `inlineItem`, `itemLink`, `block` and `inlineBlock` using a set of default rules, but you might want to customize those. For example:

- For `heading` nodes, you might want to add an anchor;
- For `code` nodes, you might want to use a custom sytax highlighting component;

In this case, you can easily override default rendering rules with the `customNodeRules` and `customMarkRules` props.

```vue
<template>
  <datocms-structured-text
    :data="data.blogPost.content"
    :customNodeRules="customNodeRules"
    :customMarkRules="customMarkRules"
  />
</template>

<script>
import { StructuredText, renderNodeRule, renderMarkRule } from "vue-datocms";
import { isHeading, isCode } from "datocms-structured-text-utils";
import { render as toPlainText } from 'datocms-structured-text-to-plain-text';
import SyntaxHighlight from './components/SyntaxHighlight';

export default {
  components: {
    "datocms-structured-text": StructuredText,
    "syntax-highlight": SyntaxHighlight,
  },
  data() {
    return {
      data: /* ... */,
      customNodeRules: [
        renderNodeRule(isHeading, ({ adapter: { renderNode: h }, node, children, key }) => {
          const anchor = toPlainText(node)
            .toLowerCase()
            .replace(/ /g, '-')
            .replace(/[^\w-]+/g, '');

          return h(
            `h${node.level}`, { key }, [
              ...children,
              h('a', { attrs: { id: anchor } }, []),
              h('a', { attrs: { href: `#${anchor}` } }, []),
            ]
          );
        }),
        renderNodeRule(isCode, ({ adapter: { renderNode: h }, node, key }) => {
          return h('syntax-highlight', {
            key,
            code: node.code,
            language: node.language,
            linesToBeHighlighted: node.highlight,
          }, []);
        }),
      ],
      customMarkRules: [
        // convert "strong" marks into <b> tags
        renderMarkRule('strong', ({ adapter: { renderNode: h }, mark, children, key }) => {
          return h('b', {key}, children);
        }),
      ],
    };
  },
};
</script>
```

Note: if you override the rules for `inlineItem`, `itemLink`, `block` or `inlineBlock` nodes, then the `renderInlineRecord`, `renderLinkToRecord`, `renderBlock`, `renderInlineBlock` props won't be considered!

## Props

| prop               | type                                                       | required                                               | description                                                                                      | default                                                                                                              |
| ------------------ | ---------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| data               | `StructuredTextGraphQlResponse \| DastNode`                | :white_check_mark:                                     | The actual [field value](https://www.datocms.com/docs/structured-text/dast) you get from DatoCMS |                                                                                                                      |
| renderInlineRecord | `({ record }) => VNode \| null`                            | Only required if document contains `inlineItem` nodes  | Convert an `inlineItem` DAST node into a VNode                                                   | `[]`                                                                                                                 |
| renderLinkToRecord | `({ record, children, transformedMeta }) => VNode \| null` | Only required if document contains `itemLink` nodes    | Convert an `itemLink` DAST node into a VNode                                                     | `null`                                                                                                               |
| renderBlock        | `({ record }) => VNode \| null`                            | Only required if document contains `block` nodes       | Convert a `block` DAST node into a VNode                                                         | `null`                                                                                                               |
| renderInlineBlock  | `({ record }) => VNode \| null`                            | Only required if document contains `inlineBlock` nodes | Convert an `inlineBlock` DAST node into a VNode                                                  | `null`                                                                                                               |
| metaTransformer    | `({ node, meta }) => Object \| null`                       | :x:                                                    | Transform `link` and `itemLink` meta property into HTML props                                    | [See function](https://github.com/datocms/structured-text/blob/main/packages/generic-html-renderer/src/index.ts#L61) |
| customNodeRules    | `Array<RenderRule>`                                        | :x:                                                    | Customize how nodes are converted in JSX (use `renderNodeRule()` to generate)                    | `null`                                                                                                               |
| customMarkRules    | `Array<RenderMarkRule>`                                    | :x:                                                    | Customize how marks are converted in JSX (use `renderMarkRule()` to generate)                    | `null`                                                                                                               |
| renderText         | `(text: string, key: string) => VNode \| string \| null`   | :x:                                                    | Convert a simple string text into a VNode                                                        | `(text) => text`                                                                                                     |

---

# Vue/Nuxt — useQuerySubscription composable for live real-time updates

Source [github]: https://raw.githubusercontent.com/datocms/vue-datocms/master/src/composables/useQuerySubscription/README.md

`useQuerySubscription` is a Vue composable that you can use to implement client-side updates of the page as soon as the content changes. It uses DatoCMS's [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api/api-reference) to receive the updated query results in real-time, and is able to reconnect in case of network failures.

Live updates are great both to get instant previews of your content while editing it inside DatoCMS, or to offer real-time updates of content to your visitors (ie. news site).

`useQuerySubscription` is based on the `subscribeToQuery` helper provided by the [datocms-listen](https://www.npmjs.com/package/datocms-listen) package that provide real-time updates for the page when the content changes. Please consult the [datocms-listen package documentation](https://www.npmjs.com/package/datocms-listen) to learn more about how to configure `subscribeToQuery`.

Live updates are great both to get instant previews of your content while editing it inside DatoCMS, or to offer real-time updates of content to your visitors (ie. news site).

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Reference](#reference)
- [Initialization options](#initialization-options)
- [Connection status](#connection-status)
- [Error object](#error-object)
- [Example](#example)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

```
npm install --save vue-datocms
```

## Reference

Import `useQuerySubscription` from `vue-datocms` and use it inside your components setup function like this:

```js
const {
  data: QueryResult | undefined,
  error: ChannelErrorData | null,
  status: ConnectionStatus,
} = useQuerySubscription(options: Options);
```

## Initialization options

| prop               | type                                                                                       | required           | description                                                                                      | default                              |
| ------------------ | ------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------ |
| enabled            | boolean                                                                                    | :x:                | Whether the subscription has to be performed or not                                              | true                                 |
| query              | string \| [`TypedDocumentNode`](https://github.com/dotansimha/graphql-typed-document-node) | :white_check_mark: | The GraphQL query to subscribe                                                                   |                                      |
| token              | string                                                                                     | :white_check_mark: | DatoCMS API token to use                                                                         |                                      |
| variables          | Object                                                                                     | :x:                | GraphQL variables for the query                                                                  |                                      |
| includeDrafts      | boolean                                                                                    | :x:                | If true, draft records will be returned                                                          |                                      |
| excludeInvalid     | boolean                                                                                    | :x:                | If true, invalid records will be filtered out                                                    |                                      |
| environment        | string                                                                                     | :x:                | The name of the DatoCMS environment where to perform the query (defaults to primary environment) |                                      |
| contentLink        | `'vercel-1'` or `undefined`                                                                | :x:                | If true, embed metadata that enable Content Link                                                 |                                      |
| baseEditingUrl     | string                                                                                     | :x:                | The base URL of the DatoCMS project                                                              |                                      |
| cacheTags          | boolean                                                                                    | :x:                | If true, receive the Cache Tags associated with the query                                        |                                      |
| initialData        | Object                                                                                     | :x:                | The initial data to use on the first render                                                      |                                      |
| reconnectionPeriod | number                                                                                     | :x:                | In case of network errors, the period (in ms) to wait to reconnect                               | 1000                                 |
| fetcher            | a [fetch-like function](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)        | :x:                | The fetch function to use to perform the registration query                                      | window.fetch                         |
| eventSourceClass   | an [EventSource-like](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) class  | :x:                | The EventSource class to use to open up the SSE connection                                       | window.EventSource                   |
| baseUrl            | string                                                                                     | :x:                | The base URL to use to perform the query                                                         | `https://graphql-listen.datocms.com` |

## Connection status

The `status` property represents the state of the server-sent events connection. It can be one of the following:

- `connecting`: the subscription channel is trying to connect
- `connected`: the channel is open, we're receiving live updates
- `closed`: the channel has been permanently closed due to a fatal error (ie. an invalid query)

## Error object

| prop     | type   | description                                             |
| -------- | ------ | ------------------------------------------------------- |
| code     | string | The code of the error (ie. `INVALID_QUERY`)             |
| message  | string | An human friendly message explaining the error          |
| response | Object | The raw response returned by the endpoint, if available |

## Example

See the query-subscription [`App.vue`](/examples/query-subscription/src/App.vue) for a usage example.

---

# Vue/Nuxt — useSiteSearch composable to query the DatoCMS Site Search API

Source [github]: https://raw.githubusercontent.com/datocms/vue-datocms/master/src/composables/useSiteSearch/README.md

`useSiteSearch` is a Vue composable that you can use to render a [DatoCMS Site Search](https://www.datocms.com/docs/site-search) widget.
The hook only handles the form logic: you are in complete and full control of how your form renders down to the very last component, class or style.

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Site Search composable](#site-search-composable)
  - [Table of Contents](#table-of-contents)
  - [Installation](#installation)
  - [Reference](#reference)
  - [Initialization options](#initialization-options)
  - [Returned data](#returned-data)
  - [Complete example](#complete-example)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

To perform the necessary API requests, this hook requires a [DatoCMS CMA Client](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients) instance, so make sure to also add the following package to your project:

```bash
npm install --save vue-datocms @datocms/cma-client-browser
```

## Reference

Import `useSiteSearch` from `vue-datocms` and use it inside your components like this:

```js
import { useSiteSearch } from 'vue-datocms';
import { buildClient } from '@datocms/cma-client-browser';

const client = buildClient({ apiToken: 'YOUR_API_TOKEN' });

const { state, error, data } = useSiteSearch({
  client,
  searchIndexId: '7497',
  // optional: by default fuzzy-search is not active
  fuzzySearch: true,
  // optional: you can omit it you only have one locale, or you want to find results in every locale
  initialState: { locale: 'en' },
  // optional: defaults to 8 search results per page
  resultsPerPage: 10,
});
```

For a complete walk-through, please refer to the [DatoCMS Site Search documentation](https://www.datocms.com/docs/site-search).

## Initialization options

| prop                | type                | required           | description                                                                                                                                | default |
| ------------------- | ------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| client              | CMA Client instance | :white_check_mark: | [DatoCMS CMA Client](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients) instance                                |         |
| searchIndexId      | string              | :white_check_mark: | The [ID of the search index](https://www.datocms.com/docs/site-search/base-integration#performing-searches) to use to find search results |         |
| fuzzySearch         | boolean             | :x:                | Whether fuzzy-search is active or not. When active, it will also find strings that approximately match the query provided.                 | false   |
| resultsPerPage      | number              | :x:                | The number of search results to show per page                                                                                              | 8       |
| initialState.query  | string              | :x:                | Initialize the form with a specific query                                                                                                  | ''      |
| initialState.locale | string              | :x:                | Initialize the form starting from a specific page                                                                                          | 0       |
| initialState.page   | string              | :x:                | Initialize the form with a specific locale selected                                                                                        | null    |

## Returned data

The hook returns an object with the following shape:

```typescript
{
  state: {
    query: string;
    locale: string | undefined;
    page: number;
  },
  error?: string,
  data?: {
    pageResults: Array<{
      id: string;
      title: string;
      titleHighlights: ResultHighlight[];
      bodyExcerpt: string;
      bodyHighlights: ResultHighlight[];
      url: string;
      raw: RawSearchResult;
    }>;
    totalResults: number;
    totalPages: number;
  },
}
```

`titleHighlights` and `bodyHighlights` have the following shape:

```typescript
type ResultHighlight = HighlightPiece[]

type HighlightPiece = {
  text: string;
  isMatch: boolean;
}
```

- The `state` property reflects the current state of the form (the current `query`, `page`, and `locale`), and offers a number of functions to change the state itself. As soon as the state of the form changes, a new API request is made to fetch the new search results;
- The `error` property returns a string in case of failure of any API request;
- The `data` property returns all the information regarding the current search results to present to the user;

## Complete example

See a more complete [`site search example`](/examples/src/SiteSearch/index.vue) for usage.

---

# Vue/Nuxt — <datocms-content-link> component for Visual Editing

Source [github]: https://raw.githubusercontent.com/datocms/vue-datocms/master/src/components/ContentLink/README.md

`<ContentLink />` is a Vue component that enables **Visual Editing** for DatoCMS content by providing click-to-edit overlays and seamless integration with the DatoCMS Web Previews plugin.

- TypeScript ready;
- Usable both client and server side;
- Compatible with vanilla Vue, Nuxt and pretty much any other Vue-based solution;
- Framework-agnostic with easy integration for Vue Router, Nuxt Router, and custom routers;

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [What is Visual Editing?](#what-is-visual-editing)
- [Out-of-the-box features](#out-of-the-box-features)
- [Installation](#installation)
- [Basic Setup](#basic-setup)
  - [Step 1: Configure your DatoCMS client](#step-1-configure-your-datocms-client)
  - [Step 2: Add the ContentLink component](#step-2-add-the-contentlink-component)
- [Usage](#usage)
  - [Framework-agnostic (no routing)](#framework-agnostic-no-routing)
  - [With Vue Router](#with-vue-router)
  - [With Nuxt](#with-nuxt)
- [Enabling click-to-edit](#enabling-click-to-edit)
- [Flash-all highlighting](#flash-all-highlighting)
- [Props](#props)
- [Advanced usage: the `useContentLink` composable](#advanced-usage-the-usecontentlink-composable)
  - [When to use the composable](#when-to-use-the-composable)
  - [API Reference](#api-reference)
  - [Example with custom integration](#example-with-custom-integration)
- [Data attributes reference](#data-attributes-reference)
  - [Developer-specified attributes](#developer-specified-attributes)
    - [`data-datocms-content-link-url`](#data-datocms-content-link-url)
    - [`data-datocms-content-link-source`](#data-datocms-content-link-source)
    - [`data-datocms-content-link-group`](#data-datocms-content-link-group)
    - [`data-datocms-content-link-boundary`](#data-datocms-content-link-boundary)
  - [Library-managed attributes](#library-managed-attributes)
    - [`data-datocms-contains-stega`](#data-datocms-contains-stega)
    - [`data-datocms-auto-content-link-url`](#data-datocms-auto-content-link-url)
- [How group and boundary resolution works](#how-group-and-boundary-resolution-works)
- [Structured Text fields](#structured-text-fields)
  - [Rule 1: Always wrap the Structured Text component in a group](#rule-1-always-wrap-the-structured-text-component-in-a-group)
  - [Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary](#rule-2-wrap-embedded-blocks-inline-records-and-inline-blocks-in-a-boundary)
- [Low-level utilities](#low-level-utilities)
  - [`decodeStega`](#decodestega)
  - [`stripStega`](#stripstega)
- [Troubleshooting](#troubleshooting)
  - [Click-to-edit overlays not appearing](#click-to-edit-overlays-not-appearing)
  - [Overlays appearing in wrong places](#overlays-appearing-in-wrong-places)
  - [Navigation not working in Web Previews plugin](#navigation-not-working-in-web-previews-plugin)
  - [Performance issues with many editable elements](#performance-issues-with-many-editable-elements)
  - [Content not clickable inside StructuredText](#content-not-clickable-inside-structuredtext)
  - [Layout issues caused by stega encoding](#layout-issues-caused-by-stega-encoding)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## What is Visual Editing?

Visual Editing transforms how content editors interact with your website. Instead of navigating through forms and fields in a CMS, editors can:

1. **See their content in context** - Preview exactly how content appears on the live site
2. **Click to edit** - Click directly on any text, image, or field to open the editor
3. **Navigate seamlessly** - Jump between pages in the preview, and the CMS follows along
4. **Get instant feedback** - Changes in the CMS are reflected immediately in the preview

This drastically improves the editing experience, especially for non-technical users who can now edit content without understanding the underlying CMS structure.

## Out-of-the-box features

- **Click-to-edit overlays**: Visual indicators showing which content is editable
- **Stega decoding**: Automatically detects and decodes editing metadata embedded in content
- **Keyboard shortcuts**: Hold Alt/Option to temporarily enable editing mode
- **Flash-all highlighting**: Show all editable areas at once for quick orientation
- **Bidirectional navigation**: Sync navigation between preview and DatoCMS editor
- **Framework-agnostic**: Works with Vue Router, Nuxt, or any routing solution
- **StructuredText integration**: Special support for complex structured content fields
- **[Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) integration**: Seamless integration with DatoCMS's editing interface

## Installation

```bash
npm install vue-datocms
```

The `@datocms/content-link` package is included as a dependency, so you don't need to install it separately.

## Basic Setup

Visual Editing requires two steps to set up:

### Step 1: Configure your DatoCMS client

When fetching content from DatoCMS, enable stega encoding to embed editing metadata:

```js
import { executeQuery } from '@datocms/cda-client';

const query = `
  query {
    page {
      title
      content
    }
  }
`;

const result = await executeQuery(query, {
  token: 'YOUR_API_TOKEN',
  environment: 'main',
  // Enable stega encoding
  contentLink: 'v1',
  // Set your site's base URL for editing links
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
```

The `contentLink: 'v1'` option enables stega encoding, which embeds invisible metadata into text fields. The `baseEditingUrl` tells DatoCMS where your project is located so edit URLs can be generated correctly. Both options are required.

### Step 2: Add the ContentLink component

Add the `<ContentLink />` component to your app. It doesn't render anything visible, but it activates the Visual Editing features:

```vue
<script setup>
import { ContentLink } from 'vue-datocms';
</script>

<template>
  <ContentLink />
  <!-- Your content here -->
</template>
```

That's it! Editors can now press and hold the Alt/Option key to temporarily activate click-to-edit mode.

## Usage

### Framework-agnostic (no routing)

For simple sites without client-side routing:

```vue
<script setup>
import { ContentLink } from 'vue-datocms';
</script>

<template>
  <ContentLink />
  <!-- Your content here -->
</template>
```

### With Vue Router

For apps using Vue Router, pass routing callbacks to enable in-plugin navigation:

```vue
<script setup>
import { ContentLink } from 'vue-datocms';
import { useRouter, useRoute } from 'vue-router';

const router = useRouter();
const route = useRoute();
</script>

<template>
  <ContentLink
    :on-navigate-to="(path) => router.push(path)"
    :current-path="route.path"
  />
  <!-- Your content here -->
</template>
```

### With Nuxt

For Nuxt applications:

```vue
<script setup>
import { ContentLink } from 'vue-datocms';

const router = useRouter();
const route = useRoute();
</script>

<template>
  <ContentLink
    :on-navigate-to="(path) => router.push(path)"
    :current-path="route.path"
  />
  <!-- Your content here -->
</template>
```

Or create a reusable component:

```vue
<!-- components/ContentLink.vue -->
<script setup>
import { ContentLink as DatoContentLink } from 'vue-datocms';

const router = useRouter();
const route = useRoute();
</script>

<template>
  <DatoContentLink
    :on-navigate-to="(path) => router.push(path)"
    :current-path="route.path"
  />
</template>
```

Then use it in your layout:

```vue
<template>
  <div>
    <ContentLink />
    <slot />
  </div>
</template>
```

## Enabling click-to-edit

By default, click-to-edit overlays are **not enabled automatically**. Editors have two ways to activate them:

1. **Alt/Option key (recommended)**: Press and hold the Alt (Windows/Linux) or Option (Mac) key to temporarily enable click-to-edit mode. Release the key to disable it. This is the most convenient method as it requires no code changes.

2. **Programmatically on mount**: Set the `enable-click-to-edit` prop to enable overlays when the component mounts:

```vue
<template>
  <ContentLink :enable-click-to-edit="true" />
</template>
```

Or with options:

```vue
<template>
  <!-- Scroll to nearest editable element if none visible -->
  <ContentLink :enable-click-to-edit="{ scrollToNearestTarget: true }" />

  <!-- Only enable on devices with hover capability (non-touch) -->
  <ContentLink :enable-click-to-edit="{ hoverOnly: true }" />

  <!-- Combine both options -->
  <ContentLink :enable-click-to-edit="{ hoverOnly: true, scrollToNearestTarget: true }" />
</template>
```

**Options:**

- `scrollToNearestTarget`: Automatically scroll to the nearest editable element if none are currently visible on screen. Helpful for long pages.
- `hoverOnly`: Only enable click-to-edit on devices that support hover (i.e., non-touch devices). This is useful to avoid showing overlays on touch devices where they may interfere with normal scrolling and tapping behavior. On touch-only devices, users can still toggle click-to-edit manually using the Alt/Option key.

## Flash-all highlighting

The flash-all feature visually highlights all editable elements with an animated effect, helping editors discover what content they can edit. This is particularly useful when first exploring a page.

To trigger flash-all, you need to use the `useContentLink` composable:

```vue
<script setup>
import { useContentLink } from 'vue-datocms';

const { flashAll } = useContentLink();

function showEditableAreas() {
  // Highlight all editable elements and scroll to the nearest one
  flashAll(true);
}
</script>

<template>
  <button @click="showEditableAreas">Show editable areas</button>
</template>
```

## Props

| Prop                   | Type                                      | Default | Description                                                                                                                                            |
| ---------------------- | ----------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `on-navigate-to`       | `(path: string) => void`                  | -       | Callback when [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) requests navigation to a different page |
| `current-path`         | `string`                                  | -       | Current pathname to sync with [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews)                         |
| `enable-click-to-edit` | `true \| { scrollToNearestTarget?: boolean, hoverOnly?: boolean }` | -       | Enable click-to-edit overlays on mount. Pass `true` or an object with options. If undefined, click-to-edit is disabled                                 |
| `strip-stega`          | `boolean`                                 | -       | Whether to strip stega encoding from text nodes after stamping                                                                                         |
| `root`                 | `Ref<ParentNode \| null \| undefined>`    | -       | Ref to limit scanning to this root element instead of the entire document                                                                              |
| `hue`                  | `number`                                  | `17`    | Hue (0–359) of the overlay accent color. Default is the DatoCMS hue (`17`). Use this to match your brand or project colors                             |

## Advanced usage: the `useContentLink` composable

For more control over Visual Editing behavior, you can use the `useContentLink` composable directly. This gives you programmatic access to all Visual Editing features.

### When to use the composable

Use the composable instead of the component when you need to:

- Programmatically enable/disable click-to-edit based on conditions
- Trigger flash-all highlighting from your UI
- Access the controller instance directly
- Integrate with custom state management
- Build custom Visual Editing UI

### API Reference

```typescript
import { useContentLink } from 'vue-datocms';

const {
  controller,              // Ref<Controller | null> - The controller instance
  enableClickToEdit,       // (options?) => void - Enable click-to-edit overlays
  disableClickToEdit,      // () => void - Disable click-to-edit overlays
  isClickToEditEnabled,    // () => boolean - Check if click-to-edit is enabled
  flashAll,                // (scrollToNearestTarget?) => void - Highlight all editable elements
  setCurrentPath,          // (path: string) => void - Notify plugin of current path
} = useContentLink({
  // enabled can be:
  // - true (default): Enable with default settings (stega encoding preserved)
  // - false: Disable the controller
  // - { stripStega: true }: Enable and strip stega encoding for clean DOM
  enabled: true,
  onNavigateTo: (path) => { /* handle navigation */ },
  root: myRootElementRef,  // Optional: limit scanning to this element
});
```

**Options:**

- `enabled?: boolean | { stripStega: boolean }` - Controls whether the controller is enabled and how it handles stega encoding:
  - `true` (default): Enables the controller with stega encoding preserved in the DOM (allows controller recreation)
  - `false`: Disables the controller completely
  - `{ stripStega: true }`: Enables the controller and permanently removes stega encoding from text nodes for clean `textContent` access
- `onNavigateTo?: (path: string) => void` - Callback when Web Previews plugin requests navigation
- `root?: Ref<ParentNode | null | undefined>` - Ref to limit scanning to this root element
- `hue?: number` - Hue (0–359) of the overlay accent color (default: `17`, the DatoCMS hue)

**Note:** The `<ContentLink />` component allows controlling stega stripping through the `strip-stega` prop. When undefined, the underlying library's default behavior is used.

### Example with custom integration

```vue
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { useContentLink } from 'vue-datocms';
import { useRouter, useRoute } from 'vue-router';

const router = useRouter();
const route = useRoute();

// State to track editing mode
const isEditingMode = ref(false);

// Initialize Visual Editing
const {
  controller,
  enableClickToEdit,
  disableClickToEdit,
  isClickToEditEnabled,
  flashAll,
  setCurrentPath,
} = useContentLink({
  enabled: true,
  onNavigateTo: (path) => router.push(path),
});

// Toggle editing mode
function toggleEditingMode() {
  if (isClickToEditEnabled()) {
    disableClickToEdit();
    isEditingMode.value = false;
  } else {
    enableClickToEdit({ scrollToNearestTarget: true });
    isEditingMode.value = true;
  }
}

// Show all editable areas
function highlightAllContent() {
  flashAll(true);
}

// Keep Web Previews plugin in sync
watch(() => route.path, (newPath) => {
  setCurrentPath(newPath);
}, { immediate: true });

// Enable editing mode on mount for editors
onMounted(() => {
  const isEditor = /* check if user is an editor */;
  if (isEditor) {
    enableClickToEdit();
    isEditingMode.value = true;
  }
});
</script>

<template>
  <div>
    <!-- Custom editing toolbar -->
    <div v-if="controller" class="editing-toolbar">
      <button @click="toggleEditingMode">
        {{ isEditingMode ? 'Disable' : 'Enable' }} Editing
      </button>
      <button @click="highlightAllContent">
        Show Editable Areas
      </button>
    </div>

    <!-- Your content here -->
  </div>
</template>
```

## Data attributes reference

This library uses several `data-datocms-*` attributes. Some are **developer-specified** (you add them to your markup), and some are **library-managed** (added automatically during DOM stamping). Here's a complete reference.

### Developer-specified attributes

These attributes are added by you in your templates/components to control how editable regions behave.

#### `data-datocms-content-link-url`

Manually marks an element as editable with an explicit edit URL. Use this for non-text fields (booleans, numbers, dates, JSON) that cannot contain stega encoding. The recommended approach is to use the `_editingUrl` field available on all records:

```graphql
query {
  product {
    id
    price
    isActive
    _editingUrl
  }
}
```

```vue
<template>
  <span :data-datocms-content-link-url="product._editingUrl">
    {{ product.price }}
  </span>
</template>
```

#### `data-datocms-content-link-source`

Attaches stega-encoded metadata without the need to render it as content. Useful for structural elements that cannot contain text (like `<video>`, `<audio>`, `<iframe>`, etc.) or when stega encoding in visible text would be problematic:

```vue
<template>
  <div :data-datocms-content-link-source="video.alt">
    <video :src="video.url" :poster="video.posterImage.url" controls />
  </div>
</template>
```

The value must be a stega-encoded string (any text field from the API will work). The library decodes the stega metadata from the attribute value and makes the element clickable to edit.

#### `data-datocms-content-link-group`

Expands the clickable area to a parent element. When the library encounters stega-encoded content, by default it makes the immediate parent of the text node clickable to edit. Adding this attribute to an ancestor makes that ancestor the clickable target instead:

```html
<article data-datocms-content-link-group>
  <!-- product.title contains stega encoding -->
  <h2>{{ product.title }}</h2>
  <p>${{ product.price }}</p>
</article>
```

Here, clicking anywhere in the `<article>` opens the editor, rather than requiring users to click precisely on the `<h2>`.

**Important:** A group should contain only one stega-encoded source. If multiple stega strings resolve to the same group, the library logs a collision warning and only the last URL wins.

#### `data-datocms-content-link-boundary`

Stops the upward DOM traversal that looks for a `data-datocms-content-link-group`, making the element where stega was found the clickable target instead. This creates an independent editable region that won't merge into a parent group (see [How group and boundary resolution works](#how-group-and-boundary-resolution-works) below for details):

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{{ page.title }}</h1>
  <section data-datocms-content-link-boundary>
    <!-- page.author contains stega encoding → resolves to URL B -->
    <span>{{ page.author }}</span>
  </section>
</div>
```

Without the boundary, clicking `page.author` would open URL A (the outer group). With the boundary, the `<span>` becomes the clickable target opening URL B.

The boundary can also be placed directly on the element that contains the stega text:

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{{ page.title }}</h1>
  <!-- page.author contains stega encoding → resolves to URL B -->
  <span data-datocms-content-link-boundary>{{ page.author }}</span>
</div>
```

Here, the `<span>` has the boundary and directly contains the stega text, so the `<span>` itself becomes the clickable target (since the starting element and the boundary element are the same).

### Library-managed attributes

These attributes are added automatically by the library during DOM stamping. You do not need to add them yourself, but you can target them in CSS or JavaScript.

#### `data-datocms-contains-stega`

Added to elements whose text content contains stega-encoded invisible characters. This attribute is only present when `stripStega` is `false` (the default), since with `stripStega: true` the characters are removed entirely. Useful for CSS workarounds — the zero-width characters can sometimes cause unexpected letter-spacing or text overflow:

```css
[data-datocms-contains-stega] {
  letter-spacing: 0 !important;
}
```

#### `data-datocms-auto-content-link-url`

Added automatically to elements that the library has identified as editable targets (through stega decoding and group/boundary resolution). Contains the resolved edit URL.

This is the automatic counterpart to the developer-specified `data-datocms-content-link-url`. The library adds `data-datocms-auto-content-link-url` wherever it can extract an edit URL from stega encoding, while `data-datocms-content-link-url` is needed for non-text fields (booleans, numbers, dates, etc.) where stega encoding cannot be embedded. Both attributes are used by the click-to-edit overlay system to determine which elements are clickable and where they link to.

## How group and boundary resolution works

When the library encounters stega-encoded content inside an element, it walks up the DOM tree from that element:

1. If it finds a `data-datocms-content-link-group`, it stops and stamps **that** element as the clickable target.
2. If it finds a `data-datocms-content-link-boundary`, it stops and stamps the **starting element** as the clickable target — further traversal is prevented.
3. If it reaches the root without finding either, it stamps the **starting element**.

Here are some concrete examples to illustrate:

**Example 1: Nested groups**

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{{ page.title }}</h1>
  <div data-datocms-content-link-group>
    <!-- page.subtitle contains stega encoding → resolves to URL B -->
    <p>{{ page.subtitle }}</p>
  </div>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.subtitle`**: walks up from `<p>`, finds the inner group first → the **inner `<div>`** becomes clickable (opens URL B). The outer group is never reached.

Each nested group creates an independent clickable region. The innermost group always wins for its own content.

**Example 2: Boundary preventing group propagation**

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{{ page.title }}</h1>
  <section data-datocms-content-link-boundary>
    <!-- page.author contains stega encoding → resolves to URL B -->
    <span>{{ page.author }}</span>
  </section>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.author`**: walks up from `<span>`, hits the `<section>` boundary → traversal stops, the **`<span>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 3: Boundary inside a group**

```html
<div data-datocms-content-link-group>
  <!-- page.description contains stega encoding → resolves to URL A -->
  <p>{{ page.description }}</p>
  <div data-datocms-content-link-boundary>
    <!-- page.footnote contains stega encoding → resolves to URL B -->
    <p>{{ page.footnote }}</p>
  </div>
</div>
```

- **`page.description`**: walks up from `<p>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.footnote`**: walks up from `<p>`, hits the boundary → traversal stops, the **`<p>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 4: Multiple stega strings without groups (collision warning)**

```html
<p>
  <!-- Both product.name and product.tagline contain stega encoding -->
  {{ product.name }}
  {{ product.tagline }}
</p>
```

Both stega-encoded strings resolve to the same `<p>` element. The library logs a console warning and the last URL wins. To fix this, wrap each piece of content in its own element:

```html
<p>
  <span>{{ product.name }}</span>
  <span>{{ product.tagline }}</span>
</p>
```

## Structured Text fields

Structured Text fields require special attention because of how stega encoding works within them:

- The DatoCMS API encodes stega information inside a single `<span>` within the structured text output. Without any configuration, only that small span would be clickable.
- Structured Text fields can contain **embedded blocks** and **inline records**, each with their own editing URL that should open a different record in the editor.

Here are the rules to follow:

### Rule 1: Always wrap the Structured Text component in a group

This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```vue
<template>
  <div data-datocms-content-link-group>
    <StructuredText :data="page.content" />
  </div>
</template>
```

### Rule 2: Wrap embedded blocks, inline records, and inline blocks in a boundary

Embedded blocks, inline records, and inline blocks have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Add `data-datocms-content-link-boundary` to prevent them from merging into the parent group.

**Why `renderLinkToRecord` doesn't need a boundary:** Record links are typically just `<a>` tags wrapping text that already belongs to the surrounding structured text. Since they don't introduce a separate editing target, there's no URL collision with the parent group and no reason to isolate them with a boundary. Clicking a record link simply opens the structured text field editor — the same behavior you'd get clicking any other text in the paragraph — which is the correct outcome since the link text is part of the structured text content itself.

```vue
<template>
  <article>
    <div v-if="data">
      <ContentLink
        :on-navigate-to="(path) => router.push(path)"
        :current-path="route.path"
      />

      <h1>{{ data.blogPost.title }}</h1>

      <div data-datocms-content-link-group>
        <datocms-structured-text
          :data="data.blogPost.content"
          :renderInlineRecord="renderInlineRecord"
          :renderLinkToRecord="renderLinkToRecord"
          :renderBlock="renderBlock"
          :renderInlineBlock="renderInlineBlock"
        />
      </div>
    </div>
  </article>
</template>

<script>
import { StructuredText, Image, ContentLink } from 'vue-datocms';
import { useRouter, useRoute } from 'vue-router';
import { request } from './lib/datocms';
import { h } from 'vue';

const query = gql`
  query {
    blogPost {
      title
      content {
        value
        links {
          ... on RecordInterface {
            __typename
            id
          }
          ... on TeamMemberRecord {
            firstName
            slug
          }
        }
        blocks {
          ... on RecordInterface {
            __typename
            id
          }
          ... on ImageRecord {
            image {
              responsiveImage(
                imgixParams: { fit: crop, w: 300, h: 300, auto: format }
              ) {
                srcSet
                webpSrcSet
                sizes
                src
                width
                height
                aspectRatio
                alt
                title
                base64
              }
            }
          }
        }
        inlineBlocks {
          ... on RecordInterface {
            __typename
            id
          }
          ... on MentionRecord {
            username
          }
        }
      }
    }
  }
`;

export default {
  components: {
    'datocms-structured-text': StructuredText,
    'datocms-image': Image,
    ContentLink,
  },
  setup() {
    const router = useRouter();
    const route = useRoute();
    return { router, route };
  },
  data() {
    return {
      data: null,
    };
  },
  methods: {
    renderInlineRecord: ({ record }) => {
      switch (record.__typename) {
        case 'TeamMemberRecord':
          return h(
            'span',
            { 'data-datocms-content-link-boundary': '' },
            [h('a', { href: `/team/${record.slug}` }, record.firstName)],
          );
        default:
          return null;
      }
    },
    renderLinkToRecord: ({ record, children, transformedMeta }) => {
      switch (record.__typename) {
        case 'TeamMemberRecord':
          return h(
            'a',
            { ...transformedMeta, href: `/team/${record.slug}` },
            children,
          );
        default:
          return null;
      }
    },
    renderBlock: ({ record }) => {
      switch (record.__typename) {
        case 'ImageRecord':
          return h(
            'div',
            { 'data-datocms-content-link-boundary': '' },
            [h('datocms-image', { data: record.image.responsiveImage })],
          );
        default:
          return null;
      }
    },
    renderInlineBlock: ({ record }) => {
      switch (record.__typename) {
        case 'MentionRecord':
          return h(
            'span',
            { 'data-datocms-content-link-boundary': '' },
            [h('code', `@${record.username}`)],
          );
        default:
          return null;
      }
    },
  },
  async mounted() {
    this.data = await request({ query });
  },
};
</script>
```

With this setup:
- Clicking the main text (paragraphs, headings, lists, and record links) opens the **structured text field editor**
- Clicking an embedded block, inline record, or inline block opens **that record's editor**

## Low-level utilities

The `vue-datocms` package re-exports utility functions from `@datocms/content-link` for working with stega-encoded content:

### `decodeStega`

Decodes stega-encoded content to extract editing metadata:

```typescript
import { decodeStega } from 'vue-datocms';

const text = "Hello, world!"; // Contains invisible stega data
const decoded = decodeStega(text);

if (decoded) {
  console.log('Editing URL:', decoded.url);
  console.log('Clean text:', decoded.cleanText);
}
```

### `stripStega`

Removes stega encoding from any data type by converting to JSON, removing all stega-encoded segments, and parsing back to the original type:

```typescript
import { stripStega } from 'vue-datocms';

// Works with strings
stripStega("Hello\u200EWorld") // "HelloWorld"

// Works with objects
stripStega({ name: "John\u200E", age: 30 })

// Works with nested structures - removes ALL stega encodings
stripStega({
  users: [
    { name: "Alice\u200E", email: "alice\u200E.com" },
    { name: "Bob\u200E", email: "bob\u200E.co" }
  ]
})

// Works with arrays
stripStega(["First\u200E", "Second\u200E", "Third\u200E"])
```

These utilities are useful when you need to:
- Extract clean text for meta tags or social sharing
- Check if content has stega encoding
- Debug Visual Editing issues
- Process stega-encoded content programmatically

## Troubleshooting

### Click-to-edit overlays not appearing

1. **Check client configuration**: Make sure you've configured your DatoCMS client with `contentLink: 'v1'` and `baseEditingUrl`
2. **Verify content is stega-encoded**: Use `decodeStega()` on a text field to check if metadata is present
3. **Enable click-to-edit**: Either press Alt/Option key or set `enable-click-to-edit` prop to `true` (e.g., `:enable-click-to-edit="true"`)
4. **Check console for errors**: Look for any JavaScript errors that might prevent the controller from initializing

### Overlays appearing in wrong places

1. **Layout shifts**: If your page layout shifts after content loads, overlays may be positioned incorrectly. Try triggering a window resize event after content loads
2. **Transformed elements**: CSS transforms on parent elements can affect overlay positioning

### Navigation not working in Web Previews plugin

1. **Check `onNavigateTo` callback**: Make sure you're passing a valid navigation function
2. **Verify `currentPath` prop**: Ensure you're passing the current route path
3. **Test router integration**: Verify that your router navigation works outside of Visual Editing

### Performance issues with many editable elements

1. **Use `root` prop**: Limit scanning to a specific container instead of the entire document
2. **Avoid enabling on mount**: Use Alt/Option key activation instead of passing options to `enable-click-to-edit` prop
3. **Debounce updates**: If you're frequently updating `currentPath`, consider debouncing the updates

### Content not clickable inside StructuredText

1. **Add edit group**: Wrap StructuredText with `data-datocms-content-link-group`
2. **Check boundaries**: Make sure you're not inadvertently blocking clicks with `data-datocms-content-link-boundary` on parent elements
3. **Verify stega encoding**: Check that your GraphQL query includes text fields with stega encoding enabled

### Layout issues caused by stega encoding

The invisible zero-width characters can cause unexpected letter-spacing or text breaking out of containers. To fix this, either use `stripStega: true`, or use CSS: `[data-datocms-contains-stega] { letter-spacing: 0 !important; }`. This attribute is automatically added to elements with stega-encoded content when `stripStega: false` (the default).

---

# astro-datocms — Astro components for DatoCMS

Source [github]: https://raw.githubusercontent.com/datocms/astro-datocms/main/README.md

[![MIT](https://img.shields.io/npm/l/@datocms/astro?style=for-the-badge)](https://github.com/datocms/astro-datocms/blob/master/LICENSE) [![NPM](https://img.shields.io/npm/v/@datocms/astro?style=for-the-badge)](https://www.npmjs.com/package/@datocms/astro)

A set of TypeScript-ready components and utilities to work faster with [DatoCMS](https://www.datocms.com/) in Astro project. Integrates seamlessly with [DatoCMS's GraphQL Content Delivery API](https://www.datocms.com/docs/content-delivery-api).

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Features](#features)
- [Installation](#installation)
- [Releasing (maintainers)](#releasing-maintainers)
- [Trying a change before it's released](#trying-a-change-before-its-released)
- [What is DatoCMS?](#what-is-datocms)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Features

`@datocms/astro` contains ready-to-use Astro components and helpers:

- [`<ContentLink />`](src/ContentLink) for Visual Editing with click-to-edit overlays
- [`<Image />`](src/Image)
- [`<Seo />`](src/Seo)
- [`<StructuredText />`](src/StructuredText)
- [`<QueryListener />`](src/QueryListener)

## Installation

```
npm install @datocms/astro
```

## Releasing (maintainers)

Every user-visible change needs a changeset: run `npx changeset` from the repo
root in the same PR, pick the bump level (`patch` is for bug fixes only, new API
surface is `minor`) and commit the file it writes under `.changeset/`. That is
where the changelog entry comes from, and it is where the bump level is decided
— not on release day. See [`.changeset/README.md`](.changeset/README.md).

To release, from an up-to-date, clean `main`, run `npm run release`. It
builds and tests, applies the pending changesets — bumping the version and
writing `CHANGELOG.md` — publishes to npm, and only then tags `vX.Y.Z`, pushes,
and creates a GitHub release whose notes come straight from that changelog
entry. An interrupted release is resumed by re-running it, never undone. Use
`npm run release:next` for a prerelease under the `next` dist-tag.

The script is
[`@datocms/release-toolchain`](https://github.com/datocms/release-toolchain),
shared with every other DatoCMS repository and pinned here by tag.

---

# Astro — Responsive <Image> component

Source [github]: https://raw.githubusercontent.com/datocms/astro-datocms/main/src/Image/README.md

`<Image>` is a TypeScript-ready Astro component specially designed to work seamlessly with DatoCMS’s [`responsiveImage` GraphQL query](https://www.datocms.com/docs/content-delivery-api/uploads#responsive-images) which optimizes image loading for your websites.

### Out-of-the-box features

- Completely native, with no JavaScript footprint
- Offers optimized version of images for browsers that support WebP/AVIF format
- Generates multiple smaller images so smartphones and tablets don’t download desktop-sized images
- Efficiently lazy loads images to speed initial page load and save bandwidth
- Holds the image position so your page doesn’t jump while images load
- Uses either blur-up or background color techniques to show a preview of the image while it loads

### Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Setup](#setup)
- [Usage](#usage)
- [Example](#example)
- [The `ResponsiveImage` object](#the-responsiveimage-object)
- [`<Image />`](#image-)
  - [Props](#props)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

### Setup

You can import the component like this:

```js
import { Image } from '@datocms/astro/Image';
```

## Usage

1. Use `<Image>` in place of the regular `<img />` tag
2. Write a GraphQL query to your DatoCMS project using the [`responsiveImage` query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#responsive-images)

The GraphQL query returns multiple thumbnails with optimized compression. The components automatically set up the "blur-up" effect as well as lazy loading of images further down the screen.

## Example

Here is a minimal starting point:

```astro
---
import { Image } from '@datocms/astro/Image';
import { executeQuery } from '@datocms/cda-client';

const query = gql`
  query {
    blogPost {
      title
      cover {
        responsiveImage(imgixParams: { fit: crop, w: 300, h: 300, auto: format }) {
          # always required
          src
          width
          height
          # not required, but strongly suggested!
          alt
          title
          # blur-up placeholder, JPEG format, base64-encoded, or...
          base64
          # background color placeholder
          bgColor
          # you can omit sizes if you explicitly pass the sizes prop to the image component
          sizes
        }
      }
    }
  }
`;

const { blogPost } = await executeQuery(query, { token: '<YOUR-API-TOKEN>' });
---

<Image data={blogPost.cover.responsiveImage} />
```

## The `ResponsiveImage` object

The `data` prop of both components expects an object with the same shape as the one returned by `responsiveImage` GraphQL call. It's up to you to make a GraphQL query that will return the properties you need for a specific use of the `<Image>` component.

- The minimum required properties for `data` are: `src`, `width` and `height`;
- `alt` and `title`, while not mandatory, are all highly suggested, so remember to use them!
- If you don't request `srcSet`, the component will auto-generate an `srcset` based on `src` + the `srcSetCandidates` prop (it can help reducing the GraphQL response size drammatically when many images are returned);
- We strongly to suggest to always specify [`{ auto: format }`](https://docs.imgix.com/apis/rendering/auto/auto#format) in your `imgixParams`, instead of requesting `webpSrcSet`, so that you can also take advantage of more performant optimizations (AVIF), without increasing GraphQL response size;
- If you request both the `bgColor` and `base64` property, the latter will take precedence, so just avoid querying both fields at the same time, as it will only make the GraphQL response bigger :wink:;
- You can avoid requesting `sizes` and directly pass a `sizes` prop to the component to reduce the GraphQL response size;

Here's a complete recap of what `responsiveImage` offers:

| property    | type    | required           | description                                                                                                                                                                                    |
| ----------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| src         | string  | :white_check_mark: | The `src` attribute for the image                                                                                                                                                              |
| width       | integer | :white_check_mark: | The width of the image                                                                                                                                                                         |
| height      | integer | :white_check_mark: | The height of the image                                                                                                                                                                        |
| alt         | string  | :x:                | Alternate text (`alt`) for the image (not required, but strongly suggested!)                                                                                                                   |
| title       | string  | :x:                | Title attribute (`title`) for the image (not required, but strongly suggested!)                                                                                                                |
| sizes       | string  | :x:                | The HTML5 `sizes` attribute for the image (omit it if you're already passing a `sizes` prop to the Image component)                                                                            |
| base64      | string  | :x:                | A base64-encoded thumbnail to offer during image loading                                                                                                                                       |
| bgColor     | string  | :x:                | The background color for the image placeholder (omit it if you're already requesting `base64`)                                                                                                 |
| srcSet      | string  | :x:                | The HTML5 `srcSet` attribute for the image (can be omitted, the Image component knows how to build it based on `src`)                                                                          |
| webpSrcSet  | string  | :x:                | The HTML5 `srcSet` attribute for the image in WebP format (deprecated, it's better to use the [`auto=format`](https://docs.imgix.com/apis/rendering/auto/auto#format) Imgix transform instead) |
| aspectRatio | float   | :x:                | The aspect ratio (width/height) of the image                                                                                                                                                   |

## `<Image />`

### Props

| prop             | type                     | default                            | required           | description                                                                                                                                                                                                                                                                                                                                                   |
| ---------------- | ------------------------ | ---------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data             | `ResponsiveImage` object |                                    | :white_check_mark: | The actual response you get from a DatoCMS `responsiveImage` GraphQL query \*\*\*\*                                                                                                                                                                                                                                                                           |
| pictureClass     | string                   | null                               | :x:                | Additional CSS class for the root `<picture>` tag                                                                                                                                                                                                                                                                                                             |
| pictureStyle     | CSS properties           | null                               | :x:                | Additional CSS rules to add to the root `<picture>` tag                                                                                                                                                                                                                                                                                                       |
| imgClass         | string                   | null                               | :x:                | Additional CSS class for the `<img>` tag                                                                                                                                                                                                                                                                                                                      |
| imgStyle         | CSS properties           | null                               | :x:                | Additional CSS rules to add to the `<img>` tag                                                                                                                                                                                                                                                                                                                |
| priority         | Boolean                  | false                              | :x:                | Disables lazy loading, and sets the image [fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) to "high"                                                                                                                                                                                                          |
| sizes            | string                   | undefined                          | :x:                | The HTML5 [`sizes`](https://web.dev/learn/design/responsive-images/#sizes) attribute for the image. Falls back to `data.sizes`, then to [`sizes="auto"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#sizes) for lazy-loaded (non-`priority`) images, so the browser picks the best `srcset` candidate from the rendered width automatically |
| usePlaceholder   | Boolean                  | true                               | :x:                | Whether the image should use a blurred image placeholder                                                                                                                                                                                                                                                                                                      |
| srcSetCandidates | Array<number>            | [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4] | :x:                | If `data` does not contain `srcSet`, the candidates for the `srcset` attribute of the image will be auto-generated based on these width multipliers                                                                                                                                                                                                           |
| referrerPolicy   | string                   | `no-referrer-when-downgrade`       | :x:                | Defines which referrer is sent when fetching the image. Defaults to `no-referrer-when-downgrade` to give more useful stats in DatoCMS Project Usages                                                                                                                                                                                                          |

---

# Astro — <Seo> component for SEO meta and favicon tags

Source [github]: https://raw.githubusercontent.com/datocms/astro-datocms/main/src/Seo/README.md

Just like the image component, `<Seo />` is a component specially designed to work seamlessly with DatoCMS’s [`_seoMetaTags` and `faviconMetaTags` GraphQL queries](https://www.datocms.com/docs/content-delivery-api/seo) so that you can handle proper SEO in your pages.

You can use `<Seo />` in your pages, and it will inject title, meta and link tags in the document's `<head>` tag.

### Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Usage](#usage)
- [Example](#example)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Usage

`<Seo />`'s `data` prop takes an array of `Tag`s in the exact form they're returned by the following [DatoCMS GraphQL API](https://www.datocms.com/docs/content-delivery-api/seo) queries:

- `_seoMetaTags` query on any record, or
- `faviconMetaTags` on the global `_site` object.

## Example

Here is an example:

```astro
---
import { Seo } from '@datocms/astro/Seo';
import { executeQuery } from '@datocms/cda-client';

const query = gql`
  query {
    page: homepage {
      title
      seo: _seoMetaTags {
        attributes
        content
        tag
      }
    }
    site: _site {
      favicon: faviconMetaTags {
        attributes
        content
        tag
      }
    }
  }
`;

const result = await executeQuery(query, { token: '<YOUR-API-TOKEN>' });
---

<Seo data={[...result.page.seo, ...result.site.favicon]} />
```

---

# Astro — <StructuredText> component to render Structured Text fields

Source [github]: https://raw.githubusercontent.com/datocms/astro-datocms/main/src/StructuredText/README.md

`<StructuredText />` is an Astro component that you can use to render the value contained inside a DatoCMS [Structured Text field type](https://www.datocms.com/docs/structured-text/dast).

### Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Setup](#setup)
- [Basic usage](#basic-usage)
- [Customization](#customization)
  - [Custom components for blocks, inline blocks, inline records or links to records](#custom-components-for-blocks-inline-blocks-inline-records-or-links-to-records)
  - [Override default rendering of nodes](#override-default-rendering-of-nodes)
  - [Strict props type checking](#strict-props-type-checking)
- [Props](#props)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

### Setup

Import the component like this:

```js
import { StructuredText } from '@datocms/astro/StructuredText';
```

## Basic usage

```astro
---
import { StructuredText } from '@datocms/astro/StructuredText';
import { executeQuery } from '@datocms/cda-client';

const query = gql`
  query {
    blogPost {
      title
      content {
        value
      }
    }
  }
`;

const { blogPost } = await executeQuery(query, { token: '<YOUR-API-TOKEN>' });
---

<article>
  <h1>{data.blogPost.title}</h1>
  <StructuredText data={data.blogPost.content} />
</article>
```

## Customization

The `<StructuredText />` component comes with a set of default components that are use to render all the nodes present in [DatoCMS Dast trees](https://www.datocms.com/docs/structured-text/dast). These default components are enough to cover most of the simple cases.

You need to use custom components in the following cases:

- you have to render blocks, inline records or links to records: there's no conventional way of rendering theses nodes, so you must create and pass custom components;
- you need to render a conventional node differently (e.g. you may want a custom render for blockquotes)

### Custom components for blocks, inline blocks, inline records or links to records

- Astro components passed in `blockComponents` will be used to render blocks and will receive a `block` prop containing the actual block data.
- Astro components passed in `inlineBlockComponents` will be used to render inline blocks and will receive a `block` prop containing the actual block data.
- Astro components passed in `inlineRecordComponents` will be used to render inline records and will receive a `record` prop containing the actual record.
- Astro components passed in `linkToRecordComponents` will be used to render links to records and will receive the following props: `node` (the actual `'inlineItem'` node), `record` (the record linked to the node), and `attrs` (the custom attributes for the link specified by the node).

```astro
---
import { StructuredText } from '@datocms/astro/StructuredText';
import { executeQuery } from '@datocms/cda-client';

import Cta from '~/components/Cta/index.astro';
import NewsletterSignup from '~/components/NewsletterSignup/index.astro';

import InlineTeamMember from '~/components/InlineTeamMember/index.astro';
import LinkToTeamMember from '~/components/LinkToTeamMember/index.astro';

const query = gql`
  query {
    blogPost {
      title
      content {
        value
        blocks {
          ... on RecordInterface {
            id
            __typename
          }
          ... on CtaRecord {
            label
            url
          }
        }
        inlineBlocks {
          ... on RecordInterface {
            id
            __typename
          }
          ... on NewsletterSignupRecord {
            title
          }
        }
        links {
          ... on RecordInterface {
            id
            __typename
          }
          ... on TeamMemberRecord {
            firstName
            slug
          }
        }
      }
    }
  }
`;

const { blogPost } = await executeQuery(query, { token: '<YOUR-API-TOKEN>' });
---

<article>
  <h1>{blogPost.title}</h1>
  <StructuredText
    data={blogPost.content}
    blockComponents={{
      CtaRecord: Cta,
    }}
    inlineBlockComponents={{
      NewsletterSignupRecord: NewsletterSignup,
    }}
    inlineRecordComponents={{
      TeamMemberRecord: InlineTeamMember,
    }}
    linkToRecordComponents={{
      TeamMemberRecord: LinkToTeamMember,
    }}
  />
</article>gql.tada
```

### Override default rendering of nodes

`<StructuredText />` automatically renders all nodes (except for `inline_item`, `item_link` and `block`) using a set of default components, that you might want to customize. For example:

- For `heading` nodes, you might want to add an anchor;
- For `code` nodes, you might want to use a custom syntax highlighting component;

In this case, you can easily override default rendering rules with the `nodeOverrides` prop.

```astro
---
import { StructuredText } from '@datocms/astro/StructuredText';
import { isHeading } from 'datocms-structured-text-utils';
import HeadingWithAnchorLink from '~/components/HeadingWithAnchorLink/index.astro';
import Code from '~/components/Code/index.astro';
---

<StructuredText
  data={blogPost.content}
  nodeOverrides={{
    heading: HeadingWithAnchorLink,
    code: Code,
  }}
/>
```

### Strict props type checking

Since [Astro doesn't support generics-typed components](https://github.com/withastro/roadmap/discussions/601) yet, you can use `ensureValidStructuredTextProps()` to strictly validate that all possible block and linked record types are managed in your `blockComponents`, `inlineRecordComponents` and `linkToRecordComponents` props.

This is especially useful when working with tools like [gql.tada](https://gql-tada.0no.co/) that provide precise typing for your `data`:

```astro
---
import { StructuredText, ensureValidStructuredTextProps } from '@datocms/astro/StructuredText';
---

<StructuredText
  {...ensureValidStructuredTextProps({
    data: blogPost.content,
    blockComponents: {
      CtaRecord: Cta,
    },
    inlineBlockComponents: {
      NewsletterSignupRecord: NewsletterSignup,
    },
    inlineRecordComponents: {
      TeamMemberRecord: InlineTeamMember,
    },
    linkToRecordComponents: {
      TeamMemberRecord: LinkToTeamMember,
    },
  })}
/>
```

## Props

| prop                   | type                             | required           | description                                                                                                                   |
| ---------------------- | -------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| data                   | `StructuredText \| DastNode`     | :white_check_mark: | The actual [field value](https://www.datocms.com/docs/structured-text/dast) you get from DatoCMS                              |
| blockComponents        | `Record<string, AstroComponent>` |                    | An object in which the keys are the `__typename` of the blocks to be rendered, and the values are the Astro components        |
| inlineBlockComponents  | `Record<string, AstroComponent>` |                    | An object in which the keys are the `__typename` of the inline blocks to be rendered, and the values are the Astro components |
| linkToRecordComponents | `Record<string, AstroComponent>` |                    | An object in which the keys are the `__typename` of the records to be rendered, and the values are the Astro components       |
| inlineRecordComponents | `Record<string, AstroComponent>` |                    | An object in which the keys are the `__typename` of the records to be rendered, and the values are the Astro components       |
| nodeOverrides          | `Record<string, AstroComponent>` |                    | An object in which the keys are the types of DAST nodes to override, and the values are the Astro components                  |
| markOverrides          | `Record<string, AstroComponent>` |                    | An object in which the keys are the types of `span` node marks to override, and the values are the Astro components           |

---

# Astro — <QueryListener> component for live real-time updates

Source [github]: https://raw.githubusercontent.com/datocms/astro-datocms/main/src/QueryListener/README.md

`<QueryListener />` is an Astro component that you can use to implement client-side reload of the page as soon as the content of a query changes. It uses DatoCMS's [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api/api-reference) to receive the updated query results in real-time, and is able to reconnect in case of network failures.

Live reloads are great to get instant previews of your content while editing it inside DatoCMS.

`<QueryListener />` is based on the `subscribeToQuery` helper provided by the [datocms-listen](https://www.npmjs.com/package/datocms-listen) package.

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Reference](#reference)
- [Initialization options](#initialization-options)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

```
npm install --save @datocms/astro
```

## Reference

Import `<QueryListener>` from `@datocms/astro` and use it inside your components setup function like this:

```astro
---
import { QueryListener } from '@datocms/astro/QueryListener';
import { executeQuery } from '@datocms/cda-client';

const query = gql`
  query {
    homepage {
      title
    }
  }
`;

const data = await executeQuery(query, { token: '<YOUR-API-TOKEN>' });
---

<h1>{data.homepage.title}</h1>

<QueryListener query={query} token="<YOUR-API-TOKEN>" />
```

## Initialization options

| prop               | type                                                                                       | required           | description                                                                                                           | default                              |
| ------------------ | ------------------------------------------------------------------------------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| enabled            | boolean                                                                                    | :x:                | Whether the subscription has to be performed or not                                                                   | true                                 |
| query              | string \| [`TypedDocumentNode`](https://github.com/dotansimha/graphql-typed-document-node) | :white_check_mark: | The GraphQL query to subscribe                                                                                        |                                      |
| token              | string                                                                                     | :white_check_mark: | DatoCMS API token to use                                                                                              |                                      |
| variables          | Object                                                                                     | :x:                | GraphQL variables for the query                                                                                       |                                      |
| includeDrafts      | boolean                                                                                    | :x:                | If true, draft records will be returned                                                                               |                                      |
| excludeInvalid     | boolean                                                                                    | :x:                | If true, invalid records will be filtered out                                                                         |                                      |
| environment        | string                                                                                     | :x:                | The name of the DatoCMS environment where to perform the query (defaults to primary environment)                      |                                      |
| contentLink        | `'vercel-1'` or `undefined`                                                                | :x:                | If true, embed metadata that enable Content Link                                                                      |                                      |
| baseEditingUrl     | string                                                                                     | :x:                | The base URL of the DatoCMS project                                                                                   |                                      |
| cacheTags          | boolean                                                                                    | :x:                | If true, receive the Cache Tags associated with the query                                                             |                                      |
| initialData        | Object                                                                                     | :x:                | The initial data to use on the first render                                                                           |                                      |
| reconnectionPeriod | number                                                                                     | :x:                | In case of network errors, the period (in ms) to wait to reconnect                                                    | 1000                                 |
| reloadDelayMs      | number                                                                                     | :x:                | Delay (in ms) before reloading the page after receiving an update. Multiple updates during the delay reset the timer. | 2000                                 |
| baseUrl            | string                                                                                     | :x:                | The base URL to use to perform the query                                                                              | `https://graphql-listen.datocms.com` |

---

# Astro — <ContentLink> component for Visual Editing

Source [github]: https://raw.githubusercontent.com/datocms/astro-datocms/main/src/ContentLink/README.md

`<ContentLink />` enables Visual Editing for your DatoCMS content by providing click-to-edit overlays. It's built on top of the framework-agnostic [`@datocms/content-link`](https://www.npmjs.com/package/@datocms/content-link) library.

## What is Visual Editing?

Visual Editing transforms how editors interact with your content by letting them see and edit it directly in the context of your website. Instead of switching between the CMS and the live site, editors can:

- **See content in context**: View draft content exactly as it appears on the website
- **Click to edit**: Click any content element to instantly open the editor for that specific field
- **Navigate seamlessly**: Browse between pages while staying in editing mode
- **Get instant feedback**: See changes immediately without page refreshes

## Out-of-the-box features

- **Click-to-edit overlays**: Visual indicators showing which content is editable
- **Stega decoding**: Automatically detects stega-encoded metadata from DatoCMS GraphQL responses
- **Keyboard shortcuts**: Hold Alt/Option key to temporarily toggle click-to-edit mode
- **Flash-all highlighting**: Animated effect to show all editable elements at once
- **Bidirectional navigation**: Sync URL changes between your preview and the DatoCMS interface
- **Framework-agnostic**: Works with any Astro setup (with or without View Transitions)
- **StructuredText integration**: Special handling for complex structured content fields
- **Web Previews plugin integration**: Automatic bidirectional communication when running inside the DatoCMS Web Previews plugin

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Basic Setup](#basic-setup)
  - [Step 1: Fetch content with Content Link metadata](#step-1-fetch-content-with-content-link-metadata)
  - [Step 2: Add the ContentLink component](#step-2-add-the-contentlink-component)
- [Usage](#usage)
- [Props](#props)
  - [`enableClickToEdit` options](#enableclicktoedit-options)
- [Data attributes reference](#data-attributes-reference)
  - [Developer-specified attributes](#developer-specified-attributes)
    - [`data-datocms-content-link-url`](#data-datocms-content-link-url)
    - [`data-datocms-content-link-source`](#data-datocms-content-link-source)
    - [`data-datocms-content-link-group`](#data-datocms-content-link-group)
    - [`data-datocms-content-link-boundary`](#data-datocms-content-link-boundary)
  - [Library-managed attributes](#library-managed-attributes)
    - [`data-datocms-contains-stega`](#data-datocms-contains-stega)
    - [`data-datocms-auto-content-link-url`](#data-datocms-auto-content-link-url)
- [How group and boundary resolution works](#how-group-and-boundary-resolution-works)
- [Structured Text fields](#structured-text-fields)
  - [Rule 1: Always wrap the Structured Text component in a group](#rule-1-always-wrap-the-structured-text-component-in-a-group)
  - [Rule 2: Wrap embedded blocks, inline blocks, and inline records in a boundary](#rule-2-wrap-embedded-blocks-inline-blocks-and-inline-records-in-a-boundary)
- [Low-level utilities](#low-level-utilities)
  - [`stripStega()` works with any data type](#stripstega-works-with-any-data-type)
- [Troubleshooting](#troubleshooting)
  - [Click-to-edit overlays not appearing](#click-to-edit-overlays-not-appearing)
  - [Navigation not syncing in Web Previews plugin](#navigation-not-syncing-in-web-previews-plugin)
  - [Content inside StructuredText not clickable](#content-inside-structuredtext-not-clickable)
  - [Layout issues caused by stega encoding](#layout-issues-caused-by-stega-encoding)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

```bash
npm install --save @datocms/astro
```

Note that `@datocms/content-link` is included as a dependency and will be installed automatically.

## Basic Setup

### Step 1: Fetch content with Content Link metadata

Make sure you pass the `contentLink` and `baseEditingUrl` options when fetching content from DatoCMS:

```astro
---
import { executeQuery } from '@datocms/cda-client';

const query = `
  query {
    blogPost {
      title
      content
    }
  }
`;

const result = await executeQuery(query, {
  token: import.meta.env.DATOCMS_API_TOKEN,
  contentLink: 'v1',
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
---
```

The `contentLink: 'v1'` option enables stega encoding, which embeds invisible metadata into text fields. The `baseEditingUrl` tells DatoCMS where your project is located so edit URLs can be generated correctly. Both options are required.

### Step 2: Add the ContentLink component

Add the `<ContentLink />` component to your page or layout. This component renders nothing visible but activates all the Visual Editing features:

```astro
---
import { ContentLink } from '@datocms/astro/ContentLink';
---

<html>
  <head>
    <!-- your head content -->
  </head>
  <body>
    <!-- your page content -->
    <ContentLink />
  </body>
</html>
```

That's it! The component will automatically:

- Scan the page for stega-encoded content
- Enable Alt/Option key toggling for click-to-edit mode
- Connect to the Web Previews plugin if running inside its iframe
- Handle navigation synchronization

## Usage

The ContentLink component works seamlessly whether or not your Astro site uses [View Transitions](https://docs.astro.build/en/guides/view-transitions/). Simply add it to your layout:

```astro
---
// src/layouts/Layout.astro
import { ContentLink } from '@datocms/astro/ContentLink';
---

<html>
  <head>
    <!-- ViewTransitions are optional -->
  </head>
  <body>
    <slot />
    <ContentLink />
  </body>
</html>
```

The component automatically handles both scenarios:

- **With View Transitions**: Listens to `astro:page-load` events and syncs the URL with the Web Previews plugin during client-side navigation
- **Without View Transitions**: Still initializes correctly and handles navigation via standard page reloads

You get the full Visual Editing experience regardless of your routing setup.

## Props

| Prop                | Type                                                                  | Default | Description                                                                                                                               |
| ------------------- | --------------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `enableClickToEdit` | `boolean \| { scrollToNearestTarget?: boolean; hoverOnly?: boolean }` | -       | Enable click-to-edit overlays on mount. Use `true` for immediate activation, or pass options object (see below)                           |
| `stripStega`        | `boolean`                                                             | `false` | Strip stega-encoded invisible characters from text content. When `true`, encoding is permanently removed (prevents controller recreation) |
| `hue`               | `number`                                                              | `17`    | Hue (0–359) of the overlay accent color. Default is the DatoCMS hue (`17`). Use this to match your brand or project colors                |

### `enableClickToEdit` options

When passing an options object to `enableClickToEdit`, the following properties are available:

| Option                  | Type      | Default | Description                                                                                                                                                                                          |
| ----------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scrollToNearestTarget` | `boolean` | `false` | Automatically scroll to the nearest editable element if none is currently visible in the viewport when click-to-edit mode is enabled                                                                 |
| `hoverOnly`             | `boolean` | `false` | Only enable click-to-edit on devices that support hover (non-touch). Uses `window.matchMedia('(hover: hover)')` to detect hover capability. On touch devices, users can still toggle with Alt/Option |

**Examples:**

```astro
<!-- Enable click-to-edit immediately -->
<ContentLink enableClickToEdit={true} />

<!-- Enable with scroll-to-nearest behavior -->
<ContentLink enableClickToEdit={{ scrollToNearestTarget: true }} />

<!-- Only enable on devices with hover capability (recommended for sites with touch users) -->
<ContentLink enableClickToEdit={{ hoverOnly: true }} />

<!-- Combine both options -->
<ContentLink enableClickToEdit={{ hoverOnly: true, scrollToNearestTarget: true }} />
```

The `hoverOnly` option is particularly useful for websites that receive traffic from both desktop and mobile users. On touch devices, the click-to-edit overlays can interfere with normal scrolling and tapping behavior. By setting `hoverOnly: true`, overlays will only appear automatically on devices with a mouse or trackpad, while touch device users can still access click-to-edit mode by pressing and holding the Alt/Option key.

## Data attributes reference

This library uses several `data-datocms-*` attributes. Some are **developer-specified** (you add them to your markup), and some are **library-managed** (added automatically during DOM stamping). Here's a complete reference.

### Developer-specified attributes

These attributes are added by you in your templates/components to control how editable regions behave.

#### `data-datocms-content-link-url`

Manually marks an element as editable with an explicit edit URL. Use this for non-text fields (booleans, numbers, dates, JSON) that cannot contain stega encoding. The recommended approach is to use the `_editingUrl` field available on all records:

```graphql
query {
  product {
    id
    price
    isActive
    _editingUrl
  }
}
```

```astro
<span data-datocms-content-link-url={product._editingUrl}>
  ${product.price}
</span>
```

#### `data-datocms-content-link-source`

Attaches stega-encoded metadata without the need to render it as content. Useful for structural elements that cannot contain text (like `<video>`, `<audio>`, `<iframe>`, etc.) or when stega encoding in visible text would be problematic:

```astro
<div data-datocms-content-link-source={video.alt}>
  <video src={video.url} poster={video.posterImage.url} controls></video>
</div>
```

The value must be a stega-encoded string (any text field from the API will work). The library decodes the stega metadata from the attribute value and makes the element clickable to edit.

#### `data-datocms-content-link-group`

Expands the clickable area to a parent element. When the library encounters stega-encoded content, by default it makes the immediate parent of the text node clickable to edit. Adding this attribute to an ancestor makes that ancestor the clickable target instead:

```html
<article data-datocms-content-link-group>
  <!-- product.title contains stega encoding -->
  <h2>{product.title}</h2>
  <p>${product.price}</p>
</article>
```

Here, clicking anywhere in the `<article>` opens the editor, rather than requiring users to click precisely on the `<h2>`.

**Important:** A group should contain only one stega-encoded source. If multiple stega strings resolve to the same group, the library logs a collision warning and only the last URL wins.

#### `data-datocms-content-link-boundary`

Stops the upward DOM traversal that looks for a `data-datocms-content-link-group`, making the element where stega was found the clickable target instead. This creates an independent editable region that won't merge into a parent group (see [How group and boundary resolution works](#how-group-and-boundary-resolution-works) below for details):

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <section data-datocms-content-link-boundary>
    <!-- page.author contains stega encoding → resolves to URL B -->
    <span>{page.author}</span>
  </section>
</div>
```

Without the boundary, clicking `page.author` would open URL A (the outer group). With the boundary, the `<span>` becomes the clickable target opening URL B.

The boundary can also be placed directly on the element that contains the stega text:

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <!-- page.author contains stega encoding → resolves to URL B -->
  <span data-datocms-content-link-boundary>{page.author}</span>
</div>
```

Here, the `<span>` has the boundary and directly contains the stega text, so the `<span>` itself becomes the clickable target (since the starting element and the boundary element are the same).

### Library-managed attributes

These attributes are added automatically by the library during DOM stamping. You do not need to add them yourself, but you can target them in CSS or JavaScript.

#### `data-datocms-contains-stega`

Added to elements whose text content contains stega-encoded invisible characters. This attribute is only present when `stripStega` is `false` (the default), since with `stripStega: true` the characters are removed entirely. Useful for CSS workarounds — the zero-width characters can sometimes cause unexpected letter-spacing or text overflow:

```css
[data-datocms-contains-stega] {
  letter-spacing: 0 !important;
}
```

#### `data-datocms-auto-content-link-url`

Added automatically to elements that the library has identified as editable targets (through stega decoding and group/boundary resolution). Contains the resolved edit URL.

This is the automatic counterpart to the developer-specified `data-datocms-content-link-url`. The library adds `data-datocms-auto-content-link-url` wherever it can extract an edit URL from stega encoding, while `data-datocms-content-link-url` is needed for non-text fields (booleans, numbers, dates, etc.) where stega encoding cannot be embedded. Both attributes are used by the click-to-edit overlay system to determine which elements are clickable and where they link to.

## How group and boundary resolution works

When the library encounters stega-encoded content inside an element, it walks up the DOM tree from that element:

1. If it finds a `data-datocms-content-link-group`, it stops and stamps **that** element as the clickable target.
2. If it finds a `data-datocms-content-link-boundary`, it stops and stamps the **starting element** as the clickable target — further traversal is prevented.
3. If it reaches the root without finding either, it stamps the **starting element**.

Here are some concrete examples to illustrate:

**Example 1: Nested groups**

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <div data-datocms-content-link-group>
    <!-- page.subtitle contains stega encoding → resolves to URL B -->
    <p>{page.subtitle}</p>
  </div>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.subtitle`**: walks up from `<p>`, finds the inner group first → the **inner `<div>`** becomes clickable (opens URL B). The outer group is never reached.

Each nested group creates an independent clickable region. The innermost group always wins for its own content.

**Example 2: Boundary preventing group propagation**

```html
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <section data-datocms-content-link-boundary>
    <!-- page.author contains stega encoding → resolves to URL B -->
    <span>{page.author}</span>
  </section>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.author`**: walks up from `<span>`, hits the `<section>` boundary → traversal stops, the **`<span>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 3: Boundary inside a group**

```html
<div data-datocms-content-link-group>
  <!-- page.description contains stega encoding → resolves to URL A -->
  <p>{page.description}</p>
  <div data-datocms-content-link-boundary>
    <!-- page.footnote contains stega encoding → resolves to URL B -->
    <p>{page.footnote}</p>
  </div>
</div>
```

- **`page.description`**: walks up from `<p>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.footnote`**: walks up from `<p>`, hits the boundary → traversal stops, the **`<p>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 4: Multiple stega strings without groups (collision warning)**

```html
<p>
  <!-- Both product.name and product.tagline contain stega encoding -->
  {product.name} {product.tagline}
</p>
```

Both stega-encoded strings resolve to the same `<p>` element. The library logs a console warning and the last URL wins. To fix this, wrap each piece of content in its own element:

```html
<p>
  <span>{product.name}</span>
  <span>{product.tagline}</span>
</p>
```

## Structured Text fields

Structured Text fields require special attention because of how stega encoding works within them:

- The DatoCMS API encodes stega information inside a single `<span>` within the structured text output. Without any configuration, only that small span would be clickable.
- Structured Text fields can contain **embedded blocks** and **inline records**, each with their own editing URL that should open a different record in the editor.

Here are the rules to follow:

### Rule 1: Always wrap the Structured Text component in a group

This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```astro
---
import { StructuredText } from '@datocms/astro/StructuredText';
---

<div data-datocms-content-link-group>
  <StructuredText data={page.content} />
</div>
```

### Rule 2: Wrap embedded blocks, inline blocks, and inline records in a boundary

Embedded blocks, inline blocks, and inline records have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Add `data-datocms-content-link-boundary` to prevent them from merging into the parent group.

**Note:** Record links (`renderLinkToRecord`) don't need a boundary. They are typically just `<a>` tags wrapping text that already belongs to the surrounding structured text. Since they don't introduce a separate editing target, there's no URL collision and no reason to isolate them from the parent group — clicking a record link's text should open the structured text field editor, just like clicking any other text in the field.

Add `data-datocms-content-link-boundary` to the root element of each component that renders a block, inline block, or inline record. For example, given a `Cta` block component:

```astro
---
// src/components/Cta.astro
const { block } = Astro.props;
---

<div data-datocms-content-link-boundary>
  <a href={block.url}>{block.label}</a>
</div>
```

For inline blocks, use a `<span>` instead of a `<div>` since they appear within inline content:

```astro
---
// src/components/NewsletterSignup.astro
const { block } = Astro.props;
---

<span data-datocms-content-link-boundary>
  <input type="email" placeholder={block.placeholder} />
</span>
```

Same for inline records:

```astro
---
// src/components/InlineTeamMember.astro
const { record } = Astro.props;
---

<span data-datocms-content-link-boundary>
  <a href={`/team/${record.slug}`}>{record.name}</a>
</span>
```

Then use these components directly in your structured text rendering:

```astro
---
import { StructuredText } from '@datocms/astro/StructuredText';
import Cta from '~/components/Cta.astro';
import NewsletterSignup from '~/components/NewsletterSignup.astro';
import InlineTeamMember from '~/components/InlineTeamMember.astro';
---

<div data-datocms-content-link-group>
  <StructuredText
    data={page.content}
    blockComponents={{
      CtaRecord: Cta,
    }}
    inlineBlockComponents={{
      NewsletterSignupRecord: NewsletterSignup,
    }}
    inlineRecordComponents={{
      TeamMemberRecord: InlineTeamMember,
    }}
  />
</div>
```

With this setup:

- Clicking the main text (paragraphs, headings, lists) opens the **structured text field editor**
- Clicking an embedded block, inline block, or inline record opens **that block/record's editor**

## Low-level utilities

The `@datocms/content-link` package provides low-level utilities for working with stega-encoded content:

```astro
---
import { decodeStega, stripStega } from '@datocms/astro/ContentLink';

const text = 'Some content with invisible stega encoding';

// Extract editing metadata from stega-encoded text
const metadata = decodeStega(text);
// Returns: { origin: string, href: string } | null

// Remove stega encoding to get clean text
const cleanText = stripStega(text);
// Returns: 'Some content with invisible stega encoding' (without zero-width characters)
---
```

**Use cases:**

- **Meta tags and social sharing**: Use `stripStega()` to clean text before adding to `<meta>` tags
- **Programmatic text processing**: Remove invisible characters before string operations
- **Debugging**: Use `decodeStega()` to inspect what editing URLs are embedded in content

### `stripStega()` works with any data type

The `stripStega()` function handles strings, objects, arrays, and primitives:

```js
// Works with strings
stripStega('Hello‎World'); // "HelloWorld"

// Works with objects
stripStega({ name: 'John‎', age: 30 });

// Works with nested structures - removes ALL stega encodings
stripStega({
  users: [
    { name: 'Alice‎', email: 'alice‎.com' },
    { name: 'Bob‎', email: 'bob‎.co' },
  ],
});

// Works with arrays
stripStega(['First‎', 'Second‎', 'Third‎']);
```

## Troubleshooting

### Click-to-edit overlays not appearing

If you don't see any overlays when holding Alt/Option:

1. **Check that Content Link is enabled in your query:**

   ```ts
   const result = await executeQuery(query, {
     contentLink: 'v1', // Must be present
     baseEditingUrl: 'https://your-project.admin.datocms.com', // Must be present
   });
   ```

2. **Verify the component is loaded:**
   Check your browser console for any errors. The component should initialize silently.

3. **Ensure you're viewing draft content:**
   Content Link metadata is only included for draft content. Make sure you're using an API token with draft access and have `includeDrafts: true` in your query options if needed.

4. **Check for conflicting CSS:**
   The overlays use z-index and absolute positioning. Make sure your site's CSS isn't hiding them.

### Navigation not syncing in Web Previews plugin

If navigation isn't syncing between your preview and the DatoCMS interface:

1. **Verify you're running inside the plugin:**
   The bidirectional connection only works when your preview is loaded inside the Web Previews plugin's iframe.

2. **Check browser console:**
   Look for any errors related to the iframe communication. The component uses `postMessage` for communication.

3. **Ensure the component is in your layout:**
   The `<ContentLink />` component should be in a layout that persists across page navigations.

### Content inside StructuredText not clickable

If structured text content isn't opening the editor:

1. **Wrap with `data-datocms-content-link-group`:**
   See [Rule 1: Always wrap the Structured Text component in a group](#rule-1-always-wrap-the-structured-text-component-in-a-group).

2. **Add boundaries for embedded blocks and inline records:**
   See [Rule 2: Wrap embedded blocks and inline records in a boundary](#rule-2-wrap-embedded-blocks-and-inline-records-in-a-boundary).

3. **Check for `data-datocms-content-link-boundary` blocking clicks:**
   Make sure you haven't accidentally added a boundary attribute that's preventing the click from reaching the group.

4. **Verify stega encoding is present:**
   Use the browser inspector to check if the structured text HTML contains zero-width characters (stega encoding). If not, check your query options.

### Layout issues caused by stega encoding

The invisible zero-width characters can cause unexpected letter-spacing or text breaking out of containers. To fix this, either use `stripStega: true`, or use CSS: `[data-datocms-contains-stega] { letter-spacing: 0 !important; }`. This attribute is automatically added to elements with stega-encoded content when `stripStega: false` (the default). See [`data-datocms-contains-stega`](#data-datocms-contains-stega) for more details.

---

# @datocms/svelte — Svelte components and stores for DatoCMS

Source [github]: https://raw.githubusercontent.com/datocms/datocms-svelte/main/README.md

![MIT](https://img.shields.io/npm/l/@datocms/svelte?style=for-the-badge) ![NPM](https://img.shields.io/npm/v/@datocms/svelte?style=for-the-badge) [![Build Status](https://img.shields.io/github/actions/workflow/status/datocms/datocms-svelte/node.js.yml?branch=main&style=for-the-badge)](https://github.com/datocms/datocms-svelte/actions/workflows/node.js.yml)

A set of components to work faster with [DatoCMS](https://www.datocms.com/) in Svelte projects.

- Works with Svelte and SvelteKit;
- Written in TypeScript;
- Usable both client and server side;

### Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

  - [Features](#features)
  - [Installation](#installation)
  - [Development](#development)
  - [Building](#building)
  - [Releasing (maintainers)](#releasing-maintainers)
  - [Trying a change before it's released](#trying-a-change-before-its-released)
- [What is DatoCMS?](#what-is-datocms)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Features

`@datocms/svelte` contains ready-to-use Svelte components and usage examples.

Components:

- [`<ContentLink />` for Visual Editing with click-to-edit overlays](src/lib/components/ContentLink)
- [`<Image />` and `<NakedImage />`](src/lib/components/Image)
- [`<VideoPlayer />`](src/lib/components/VideoPlayer)
- [`<StructuredText />`](src/lib/components/StructuredText)
- [`<Head />`](src/lib/components/Head)

Stores:

- [`querySubscription`](src/lib/stores/querySubscription)

## Installation

```
npm install @datocms/svelte
```

## Development

This repository contains some examples in the `app/routes` folder. You can use them to locally test your changes to the package:

```bash
npm run dev
```

## Building

To create a production version of this library:

```bash
npm run build
```

## Releasing (maintainers)

Every user-visible change needs a changeset: run `npx changeset` from the repo
root in the same PR, pick the bump level (`patch` is for bug fixes only, new API
surface is `minor`) and commit the file it writes under `.changeset/`. That is
where the changelog entry comes from, and it is where the bump level is decided
— not on release day. See [`.changeset/README.md`](.changeset/README.md).

To release, from an up-to-date, clean `main`, run `npm run release`. It
builds and tests, applies the pending changesets — bumping the version and
writing `CHANGELOG.md` — publishes to npm, and only then tags `vX.Y.Z`, pushes,
and creates a GitHub release whose notes come straight from that changelog
entry. An interrupted release is resumed by re-running it, never undone. Use
`npm run release:next` for a prerelease under the `next` dist-tag.

The script is
[`@datocms/release-toolchain`](https://github.com/datocms/release-toolchain),
shared with every other DatoCMS repository and pinned here by tag.

---

# Svelte — Responsive <Image> and <NakedImage> components

Source [github]: https://raw.githubusercontent.com/datocms/datocms-svelte/main/src/lib/components/Image/README.md

`<Image>` and `<NakedImage />` are Svelte component specially designed to work seamlessly with DatoCMS’s [`responsiveImage` GraphQL query](https://www.datocms.com/docs/content-delivery-api/uploads#responsive-images) which optimizes image loading for your websites.

- TypeScript ready;
- Usable both client and server side;
- Compatible with vanilla Svelte and Sveltekit;

### Out-of-the-box features

- Offers optimized version of images for browsers that support WebP/AVIF format
- Generates multiple smaller images so smartphones and tablets don’t download desktop-sized images
- Efficiently lazy loads images to speed initial page load and save bandwidth
- Holds the image position so your page doesn’t jump while images load
- Uses either blur-up or background color techniques to show a preview of the image while it loads

### Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

  - [Setup](#setup)
- [Usage](#usage)
- [`<Image />` vs `<NakedImage />`](#image--vs-nakedimage-)
- [Example](#example)
- [The `ResponsiveImage` object](#the-responsiveimage-object)
- [`<NakedImage />`](#nakedimage-)
  - [Props](#props)
  - [Events](#events)
- [`<Image />`](#image-)
  - [Props](#props-1)
  - [Events](#events-1)
  - [Layout mode](#layout-mode)
  - [Intersection Observer](#intersection-observer)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

### Setup

You can import the components like this:

```js
import { Image, NakedImage } from '@datocms/svelte';
```

## Usage

1. Use `<Image>` or `<NakedImage />` in place of the regular `<img />` tag
2. Write a GraphQL query to your DatoCMS project using the [`responsiveImage` query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#responsive-images)

The GraphQL query returns multiple thumbnails with optimized compression. The components automatically set up the "blur-up" effect as well as lazy loading of images further down the screen.

## `<Image />` vs `<NakedImage />`

Even though their purpose is the same, there are some significant differences between these two components. Depending on your specific needs, you can choose to use one or the other:

- `<NakedImage />` generates minimum JS footprint, outputs a single `<picture />` element and implements lazy-loading using the native [`loading="lazy"` attribute](https://web.dev/articles/browser-level-image-lazy-loading). The placeholder is set as the background to the image itself.
- `<Image />` has the ability to set a cross-fade effect between the placeholder and the original image, but at the cost of generating more complex HTML output composed of multiple elements around the main `<picture />` element. It also implements lazy-loading through `IntersectionObserver`, which allows customization of the thresholds at which lazy loading occurs.

## Example

For a fully working example take a look at [`routes` directory](https://github.com/datocms/datocms-svelte/tree/main/src/routes/image/+page.svelte).

Here is a minimal starting point:

```svelte
<script>

import { onMount } from 'svelte';

import { Image, NakedImage } from '@datocms/svelte';

const query = gql`
  query {
    blogPost {
      title
      cover {
        responsiveImage(
          imgixParams: { fit: crop, w: 300, h: 300, auto: format }
        ) {
          # always required
          src
          width
          height
          # not required, but strongly suggested!
          alt
          title
          # blur-up placeholder, JPEG format, base64-encoded, or...
          base64
          # background color placeholder
          bgColor
          # you can omit `sizes` if you explicitly pass the `sizes` prop to the image component
          sizes
        }
      }
    }
  }
`;

export let data = null;

onMount(async () => {
  const response = await fetch('https://graphql.datocms.com/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: "Bearer AN_API_TOKEN",
    },
    body: JSON.stringify({ query })
  })

  const json = await response.json()

  data = json.data;
});

</script>

{#if data}
	<Image data={data.blogPost.cover.responsiveImage} />
	<NakedImage data={data.blogPost.cover.responsiveImage} />
{/if}
```

## The `ResponsiveImage` object

The `data` prop of both components expects an object with the same shape as the one returned by `responsiveImage` GraphQL call. It's up to you to make a GraphQL query that will return the properties you need for a specific use of the `<datocms-image>` component.

- The minimum required properties for `data` are: `src`, `width` and `height`;
- `alt` and `title`, while not mandatory, are all highly suggested, so remember to use them!
- If you don't request `srcSet`, the component will auto-generate an `srcset` based on `src` + the `srcSetCandidates` prop (it can help reducing the GraphQL response size drammatically when many images are returned);
- We strongly to suggest to always specify [`{ auto: format }`](https://docs.imgix.com/apis/rendering/auto/auto#format) in your `imgixParams`, instead of requesting `webpSrcSet`, so that you can also take advantage of more performant optimizations (AVIF), without increasing GraphQL response size;
- If you request both the `bgColor` and `base64` property, the latter will take precedence, so just avoid querying both fields at the same time, as it will only make the GraphQL response bigger :wink:;
- You can avoid requesting `sizes` and directly pass a `sizes` prop to the component to reduce the GraphQL response size;

Here's a complete recap of what `responsiveImage` offers:

| property    | type    | required           | description                                                                                                                                                                                    |
| ----------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| src         | string  | :white_check_mark: | The `src` attribute for the image                                                                                                                                                              |
| width       | integer | :white_check_mark: | The width of the image                                                                                                                                                                         |
| height      | integer | :white_check_mark: | The height of the image                                                                                                                                                                        |
| alt         | string  | :x:                | Alternate text (`alt`) for the image (not required, but strongly suggested!)                                                                                                                   |
| title       | string  | :x:                | Title attribute (`title`) for the image (not required, but strongly suggested!)                                                                                                                |
| sizes       | string  | :x:                | The HTML5 `sizes` attribute for the image (omit it if you're already passing a `sizes` prop to the Image component)                                                                            |
| base64      | string  | :x:                | A base64-encoded thumbnail to offer during image loading                                                                                                                                       |
| bgColor     | string  | :x:                | The background color for the image placeholder (omit it if you're already requesting `base64`)                                                                                                 |
| srcSet      | string  | :x:                | The HTML5 `srcSet` attribute for the image (can be omitted, the Image component knows how to build it based on `src`)                                                                          |
| webpSrcSet  | string  | :x:                | The HTML5 `srcSet` attribute for the image in WebP format (deprecated, it's better to use the [`auto=format`](https://docs.imgix.com/apis/rendering/auto/auto#format) Imgix transform instead) |
| aspectRatio | float   | :x:                | The aspect ratio (width/height) of the image                                                                                                                                                   |

## `<NakedImage />`

### Props

| prop             | type                     | default                            | required           | description                                                                                                                                          |
| ---------------- | ------------------------ | ---------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| data             | `ResponsiveImage` object |                                    | :white_check_mark: | The actual response you get from a DatoCMS `responsiveImage` GraphQL query \*\*\*\*                                                                  |
| pictureClass     | string                   | null                               | :x:                | Additional CSS class for the root `<picture>` tag                                                                                                    |
| pictureStyle     | CSS properties           | null                               | :x:                | Additional CSS rules to add to the root `<picture>` tag                                                                                              |
| imgClass         | string                   | null                               | :x:                | Additional CSS class for the `<img>` tag                                                                                                             |
| imgCtyle         | CSS properties           | null                               | :x:                | Additional CSS rules to add to the `<img>` tag                                                                                                       |
| priority         | Boolean                  | false                              | :x:                | Disables lazy loading, and sets the image [fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) to "high" |
| sizes            | string                   | undefined                          | :x:                | The HTML5 [`sizes`](https://web.dev/learn/design/responsive-images/#sizes) attribute for the image (will be used `data.sizes` as a fallback)         |
| usePlaceholder   | Boolean                  | true                               | :x:                | Whether the image should use a blurred image placeholder                                                                                             |
| srcSetCandidates | Array<number>            | [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4] | :x:                | If `data` does not contain `srcSet`, the candidates for the `srcset` attribute of the image will be auto-generated based on these width multipliers  |
| referrerPolicy   | string                   | `no-referrer-when-downgrade`       | :x:                | Defines which referrer is sent when fetching the image. Defaults to `no-referrer-when-downgrade` to give more useful stats in DatoCMS Project Usages |

### Events

| prop  | description                                 |
| ----- | ------------------------------------------- |
| @load | Emitted when the image has finished loading |

## `<Image />`

### Props

| prop                  | type                                             | default                            | required           | description                                                                                                                                                                                                                                                                                   |
| --------------------- | ------------------------------------------------ | ---------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| data                  | `ResponsiveImage` object                         |                                    | :white_check_mark: | The actual response you get from a DatoCMS `responsiveImage` GraphQL query.                                                                                                                                                                                                                   |
| class                 | string                                           | null                               | :x:                | Additional CSS class of root node                                                                                                                                                                                                                                                             |
| style                 | string                                           | null                               | :x:                | Additional CSS rules to add to the root node                                                                                                                                                                                                                                                  |
| pictureClass          | string                                           | null                               | :x:                | Additional CSS class for the inner `<picture />` tag                                                                                                                                                                                                                                          |
| pictureStyle          | string                                           | null                               | :x:                | Additional CSS rules to add to the inner `<picture />` tag                                                                                                                                                                                                                                    |
| imgClass              | string                                           | null                               | :x:                | Additional CSS class for the image inside the `<picture />` tag                                                                                                                                                                                                                               |
| imgStyle              | string                                           | null                               | :x:                | Additional CSS rules to add to the image inside the `<picture />` tag                                                                                                                                                                                                                         |
| layout                | 'intrinsic' \| 'fixed' \| 'responsive' \| 'fill' | "intrinsic"                        | :x:                | The layout behavior of the image as the viewport changes size                                                                                                                                                                                                                                 |
| fadeInDuration        | integer                                          | 500                                | :x:                | Duration (in ms) of the fade-in transition effect upoad image loading                                                                                                                                                                                                                         |
| intersectionThreshold | float                                            | 0                                  | :x:                | Indicate at what percentage of the placeholder visibility the loading of the image should be triggered. A value of 0 means that as soon as even one pixel is visible, the callback will be run. A value of 1.0 means that the threshold isn't considered passed until every pixel is visible. |
| intersectionMargin    | string                                           | "0px 0px 0px 0px"                  | :x:                | Margin around the placeholder. Can have values similar to the CSS margin property (top, right, bottom, left). The values can be percentages. This set of values serves to grow or shrink each side of the placeholder element's bounding box before computing intersections.                  |
| lazyLoad              | Boolean                                          | true                               | :x:                | Wheter enable lazy loading or not                                                                                                                                                                                                                                                             |
| explicitWidth         | Boolean                                          | false                              | :x:                | Wheter the image wrapper should explicitely declare the width of the image or keep it fluid                                                                                                                                                                                                   |
| objectFit             | String                                           | null                               | :x:                | Defines how the image will fit into its parent container when using layout="fill"                                                                                                                                                                                                             |
| objectPosition        | String                                           | null                               | :x:                | Defines how the image is positioned within its parent element when using layout="fill".                                                                                                                                                                                                       |
| priority              | Boolean                                          | false                              | :x:                | Disables lazy loading, and sets the image [fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) to "high"                                                                                                                                          |
| srcSetCandidates      | Array<number>                                    | [0.25, 0.5, 0.75, 1, 1.5, 2, 3, 4] | :x:                | If `data` does not contain `srcSet`, the candidates for the `srcset` attribute of the image will be auto-generated based on these width multipliers                                                                                                                                           |
| sizes                 | string                                           | undefined                          | :x:                | The HTML5 [`sizes`](https://web.dev/learn/design/responsive-images/#sizes) attribute for the image (will be used `data.sizes` as a fallback)                                                                                                                                                  |
| onLoad                | () => void                                       | undefined                          | :x:                | Function triggered when the image has finished loading                                                                                                                                                                                                                                        |
| usePlaceholder        | Boolean                                          | true                               | :x:                | Whether the component should use a blurred image placeholder                                                                                                                                                                                                                                  |
| referrerPolicy        | string                                           | `no-referrer-when-downgrade`       | :x:                | Defines which referrer is sent when fetching the image. Defaults to `no-referrer-when-downgrade` to give more useful stats in DatoCMS Project Usages                                                                                                                                          |

### Events

| prop  | description                                 |
| ----- | ------------------------------------------- |
| @load | Emitted when the image has finished loading |

---

### Layout mode

With the `layout` property, you can configure the behavior of the image as the viewport changes size:

- When `intrinsic`, the image will scale the dimensions down for smaller viewports, but maintain the original dimensions for larger viewports.
- When `fixed`, the image dimensions will not change as the viewport changes (no responsiveness) similar to the native `img` element.
- When `responsive` (default behaviour), the image will scale the dimensions down for smaller viewports and scale up for larger viewports.
- When `fill`, the image will stretch both width and height to the dimensions of the parent element, provided the parent element is relative.
  - This is usually paired with the `objectFit` and `objectPosition` properties.
  - Ensure the parent element has `position: relative` in their stylesheet.

### Intersection Observer

`IntersectionObserver` is the API used to determine if the image is inside the viewport or not. [Browser support is really good](https://caniuse.com/intersectionobserver): with Safari adding support in 12.1, all major browsers now support `IntersectionObserver` natively.

If `IntersectionObserver` object is not available, the component treats the image as it's always visible in the viewport. Feel free to add a [polyfill](https://www.npmjs.com/package/intersection-observer) so that it will also 100% work on older versions of iOS and IE11.

---

# Svelte — <VideoPlayer> component for Mux-encoded videos

Source [github]: https://raw.githubusercontent.com/datocms/datocms-svelte/main/src/lib/components/VideoPlayer/README.md

`<VideoPlayer />` is a Svelte component specially designed to work seamlessly
with DatoCMS’s [`video` GraphQL
query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#videos)
that optimizes video streaming for your sites.

To stream videos, DatoCMS partners with MUX, a video CDN that serves optimized
streams to your users. Our component is a wrapper around [MUX's video
player](https://github.com/muxinc/elements/blob/main/packages/mux-player/README.md)
[web
component](https://developer.mozilla.org/en-US/docs/Web/API/Web_components). It
takes care of the details for you, and this is our recommended way to serve
optimal videos to your users.

## Out-of-the-box features

- Offers optimized streaming so smartphones and tablets don’t request desktop-sized videos
- Lazy loads the underlying video player web component and the video to be
  played to speed initial page load and save bandwidth
- Holds the video position so your page doesn’t jump while the player loads
- Uses blur-up technique to show a placeholder of the video while it loads

### Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Usage](#usage)
- [Example](#example)
- [Props](#props)
- [Opt-in Viewer Analytics](#opt-in-viewer-analytics)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

```sh {"id":"01HP46D8MDP5Y76HY788MWNDMX"}
npm install --save @datocms/svelte @mux/mux-player
```

`@mux/mux-player` is a [peer dependency](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependencies) for `@datocms/svelte`: so you're expected to add it to your project.

## Usage

1. Import `VideoPlayer` from `@datocms/svelte` and use it in your app
2. Write a GraphQL query to your DatoCMS project using the [`video` query](https://www.datocms.com/docs/content-delivery-api/images-and-videos#videos)

The GraphQL query returns data that the `VideoPlayer` component automatically uses to properly size the player, set up a “blur-up” placeholder as well as lazy loading the video.

## Example

```svelte {"id":"01HP46D8MDP5Y76HY78BNPWHB2"}
<script>

import { onMount } from 'svelte';

import { VideoPlayer } from '@datocms/svelte';

const query = gql`
  query {
    blogPost {
      title
      cover {
        video {
          # required: this field identifies the video to be played
          muxPlaybackId

          # all the other fields are not required but:

          # if provided, title is displayed in the upper left corner of the video
          title

          # if provided, width and height are used to define the aspect ratio of the
          # player, so to avoid layout jumps during the rendering.
          width
          height

          # if provided, it shows a blurred placeholder for the video
          blurUpThumb

          # if provided, it enables DatoCMS Content Link for click-to-edit overlays
          alt

          # you can include more data here: they will be ignored by the component
        }
      }
    }
  }
`;

export let data = null;

onMount(async () => {
  const response = await fetch('https://graphql.datocms.com/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: "Bearer AN_API_TOKEN",
    },
    body: JSON.stringify({ query })
  })

  const json = await response.json()

  data = json.data;
});

</script>

<article>
  {#if data}
    <h1>{{ data.blogPost.title }}</h1>
    <VideoPlayer data={data.blogPost.cover.video} />
  {/if}
</article>
```

## Props

The `<VideoPlayer />` component supports as props all the [attributes](https://github.com/muxinc/elements/blob/main/packages/mux-player/REFERENCE.md)
of the `<mux-player />` web component, plus
`data`, which is meant to receive data directly in the shape they are provided
by DatoCMS GraphQL API.

`<VideoPlayer />` uses the `data` prop to generate a set of attributes for the
inner `<mux-player />`.

| prop   | type           | required           | description                                                      | default |
| ------ | -------------- | ------------------ | ---------------------------------------------------------------- | ------- |
| data   | `Video` object | :white_check_mark: | The actual response you get from a DatoCMS `video` GraphQL query |         |
| paused | `boolean`      |                    | Control to play or pause the video                               |         |

`<VideoPlayer />` generate some default attributes:

- when not declared, the `disableCookies` prop is true, unless you explicitly
  set the prop to `false` (therefore it generates a `disable-cookies` attribute)
- when not declared, the `disableTracking` prop is true, unelss you explicitly
  set it to `false` (so, it normally generates a `disable-tracking` attribute)
- `preload` defaults to `metadata`, for an optimal UX experience together with saved bandwidth
- the video height and width, when available in the `data` props, are used to
  set a default `aspect-ratio: [width] / [height];` for the `<mux-player />`'s
  `style` attribute

All the other props are forwarded to the `<mux-player />` web component that is used internally.

## Opt-in Viewer Analytics

This `<VideoPlayer/>` component can OPTIONALLY collect clientside [playback and engagement metrics](https://www.mux.com/data#TechSpecs) such as playback percentages, user agents, and geography.

These analytics are **disabled** by default. To enable them, you must opt in to [Mux Data](https://www.mux.com/data) integration by creating a Mux Data account (free) and providing its `envKey` to the component.

For details and setup instructions, please see our documentation on **[Streaming Video Analytics with Mux Data](https://www.datocms.com/docs/streaming-videos/streaming-video-analytics-with-mux-data)**.

---

# Svelte — <StructuredText> component to render Structured Text fields

Source [github]: https://raw.githubusercontent.com/datocms/datocms-svelte/main/src/lib/components/StructuredText/README.md

`StructuredText />` is a Svelte component that you can use to render the value contained inside a DatoCMS [Structured Text field type](https://www.datocms.com/docs/structured-text/dast).

### Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

  - [Setup](#setup)
- [Basic usage](#basic-usage)
- [Customization](#customization)
  - [Custom components for blocks](#custom-components-for-blocks)
  - [Override default rendering of nodes](#override-default-rendering-of-nodes)
- [Props](#props)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

### Setup

Import the component like this:

```js
import { StructuredText } from '@datocms/svelte';
```

## Basic usage

```svelte
<script>

import { onMount } from 'svelte';

import { StructuredText } from '@datocms/svelte';

const query = `
  query {
    blogPost {
      title
      content {
        value
      }
    }
  }
`;

export let data = null;

onMount(async () => {
  const response = await fetch('https://graphql.datocms.com/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: "Bearer AN_API_TOKEN",
    },
    body: JSON.stringify({ query })
  })

  const json = await response.json()

  data = json.data;
});

</script>

<article>
  {#if data}
    <h1>{{ data.blogPost.title }}</h1>
    <StructuredText data={data.blogPost.content} />
  {/if}
</article>
```

## Customization

The `<StructuredText />` component comes with a set of default components that are use to render all the nodes present in [DatoCMS Dast trees](https://www.datocms.com/docs/structured-text/dast). These default components are enough to cover most of the simple cases.

You need to use custom components in the following cases:

- you have to render blocks, inline items or item links: there's no conventional way of rendering theses nodes, so you must create and pass custom components;
- you need to render a conventional node differently (e.g. you may want a custom render for blockquotes)

### Custom components for blocks

Here is an example using custom components for blocks, inline blocks, inline records and links to records. Take a look at the [test fixtures](https://github.com/datocms/datocms-svelte/tree/main/src/lib/components/StructuredText/__tests__/__fixtures__) to see examples on how to implement these components.

```svelte
<script>
import { onMount } from 'svelte';
import { executeQuery } from '@datocms/cda-client';

import { isBlock, isInlineItem, isItemLink } from 'datocms-structured-text-utils';

import { StructuredText } from '@datocms/svelte';

import Block from './Block.svelte';
import InlineItem from './InlineItem.svelte';
import ItemLink from './ItemLink.svelte';

const query = `
  query {
    blogPost {
      title
      content {
        value
        links {
          ... on RecordInterface {
            id
            __typename
          }
          ... on TeamMemberRecord {
            firstName
            slug
          }
        }
        blocks {
          ... on RecordInterface {
            id
            __typename
          }
          ... on CtaRecord {
            title
            url
          }
        }
        inlineBlocks {
          ... on RecordInterface {
            id
            __typename
          }
          ... on MentionRecord {
            username
          }
        }
      }
    }
  }
`;

export let data = null;

onMount(async () => {
  data = await executeQuery(query, { token: '<YOUR-API-TOKEN>' });
});

</script>

<article>
  {#if data}
    <h1>{{ data.blogPost.title }}</h1>
    <datocms-structured-text
      data={data.blogPost.content}
      components={[
        [isInlineItem, InlineItem],
        [isItemLink, ItemLink],
        [isBlock, Block]
        [isInlineBlock, InlineBlock]
      ]}
    />
  {/if}
</article>
```

### Override default rendering of nodes

`<StructuredText />` automatically renders all nodes (except for `inlineItem`, `itemLink`, `block` and `inlineBlock`) using a set of default components, that you might want to customize. For example:

- For `heading` nodes, you might want to add an anchor;
- For `code` nodes, you might want to use a custom syntax highlighting component;

In this case, you can easily override default rendering rules with the `components` props. See test fixtures for example implementations of custom components (e.g. [this special heading component](https://github.com/datocms/datocms-svelte/blob/main/src/lib/components/StructuredText/__tests__/__fixtures__/IncreasedLevelHeading.svelte)).

```svelte
<script>
	import { isHeading, isCode } from 'datocms-structured-text-utils';

	import Heading from './Heading.svelte';
	import Code from './Code.svelte';

	export let data;
</script>

<StructuredText
	data={data.blogPost.content}
	components={[
		[isHeading, Heading],
		[isCode, Code]
	]}
/>
```

## Props

| prop       | type                                                                                                        | required                                                                                | description                                                                                      | default |
| ---------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------- |
| data       | `StructuredText \| DastNode`                                                                                | :white_check_mark:                                                                      | The actual [field value](https://www.datocms.com/docs/structured-text/dast) you get from DatoCMS |         |
| components | [`PredicateComponentTuple[] \| null`](https://github.com/datocms/datocms-svelte/blob/main/src/lib/index.ts) | Only required if data contains `block`, `inlineBlock`, `inlineItem` or `itemLink` nodes | Array of tuples formed by a predicate function and custom component                              | `[]`    |

---

# Svelte — <Head> component for SEO meta and favicon tags

Source [github]: https://raw.githubusercontent.com/datocms/datocms-svelte/main/src/lib/components/Head/README.md

Just like the image component, `<Head />` is a component specially designed to work seamlessly with DatoCMS’s [`_seoMetaTags` and `faviconMetaTags` GraphQL queries](https://www.datocms.com/docs/content-delivery-api/seo) so that you can handle proper SEO in your pages.

You can use `<Head />` your components, and it will inject title, meta and link tags in the document's `<head></head>` tag.

### Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Usage](#usage)
- [Example](#example)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Usage

`<Head />`'s `data` prop takes an array of `Tag`s in the exact form they're returned by the following [DatoCMS GraphQL API](https://www.datocms.com/docs/content-delivery-api/seo) queries:

- `_seoMetaTags` query on any record, or
- `faviconMetaTags` on the global `_site` object.

## Example

Here is an example:

```svelte
<script>
	import { onMount } from 'svelte';

	import { Head } from '@datocms/svelte';

	const query = `
    query {
      page: homepage {
        title
        seo: _seoMetaTags {
          attributes
          content
          tag
        }
      }
      site: _site {
        favicon: faviconMetaTags {
          attributes
          content
          tag
        }
      }
    }
  `;

	export let data = null;

	onMount(async () => {
		const response = await fetch('https://graphql.datocms.com/', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json',
				Authorization: 'Bearer AN_API_TOKEN'
			},
			body: JSON.stringify({ query })
		});

		const json = await response.json();

		data = [...json.data.page.seo, ...json.data.site.favicon];
	});
</script>

<Head {data} />
```

---

# Svelte — querySubscription store for live real-time updates

Source [github]: https://raw.githubusercontent.com/datocms/datocms-svelte/main/src/lib/stores/querySubscription/README.md

`querySubscription` returns a Svelte store that you can use to implement client-side updates of the page as soon as the content changes. It uses DatoCMS's [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api/api-reference) to receive the updated query results in real-time, and is able to reconnect in case of network failures.

Live updates are great both to get instant previews of your content while editing it inside DatoCMS, or to offer real-time updates of content to your visitors (ie. news site).

## Table of Contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Reference](#reference)
- [Initialization options](#initialization-options)
- [Disabling the subscription](#disabling-the-subscription)
- [Connection status](#connection-status)
- [Error object](#error-object)
- [Example](#example)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Reference

Import `querySubscription` from `@datocms/svelte` and use it inside your components like this:

```js
import { querySubscription } from '@datocms/svelte';

const subscription = querySubscription(options: Options);
```

## Initialization options

| prop               | type                                                                                       | required           | description                                                                                      | default                              |
| ------------------ | ------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------ |
| enabled            | boolean                                                                                    | :x:                | Whether the subscription has to be performed or not (see [Disabling the subscription](#disabling-the-subscription)) | true                                 |
| query              | string \| [`TypedDocumentNode`](https://github.com/dotansimha/graphql-typed-document-node) | :white_check_mark: | The GraphQL query to subscribe                                                                   |                                      |
| token              | string                                                                                     | :white_check_mark: | DatoCMS API token to use                                                                         |                                      |
| variables          | Object                                                                                     | :x:                | GraphQL variables for the query                                                                  |                                      |
| includeDrafts      | boolean                                                                                    | :x:                | If true, draft records will be returned                                                          |                                      |
| excludeInvalid     | boolean                                                                                    | :x:                | If true, invalid records will be filtered out                                                    |                                      |
| environment        | string                                                                                     | :x:                | The name of the DatoCMS environment where to perform the query (defaults to primary environment) |                                      |
| contentLink        | `'v1'`                                                                                     | :x:                | If set, embed metadata that enable [Content Link](https://www.datocms.com/docs/content-delivery-api/api-endpoints#content-link) |                                      |
| baseEditingUrl     | string                                                                                     | :x:                | The base URL of the DatoCMS project                                                              |                                      |
| cacheTags          | boolean                                                                                    | :x:                | If true, receive the Cache Tags associated with the query                                        |                                      |
| initialData        | Object                                                                                     | :x:                | The initial data to use on the first render                                                      |                                      |
| reconnectionPeriod | number                                                                                     | :x:                | In case of network errors, the period (in ms) to wait to reconnect                               | 1000                                 |
| fetcher            | a [fetch-like function](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)        | :x:                | The fetch function to use to perform the registration query                                      | window.fetch                         |
| eventSourceClass   | an [EventSource-like](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) class  | :x:                | The EventSource class to use to open up the SSE connection                                       | window.EventSource                   |
| baseUrl            | string                                                                                     | :x:                | The base URL to use to perform the query                                                         | `https://graphql-listen.datocms.com` |

## Disabling the subscription

Real-time updates are usually only wanted in a specific context — say, when Draft Mode is active — while regular visitors should just get the content that was fetched on the server. Instead of conditionally calling the store (which you cannot do: `querySubscription` calls `onMount`, so it must be invoked unconditionally during component initialization), pass `enabled: false`:

```js
const subscription = querySubscription({
  enabled: false,
  initialData: data.page // returned by your `load` function
});
```

When `enabled` is `false`:

- no connection is ever opened, and no API token is needed;
- the store emits `initialData` as its `data`, and `closed` as its `status`;
- every other option becomes optional. TypeScript enforces this: `QuerySubscriptionOptions` is a union of `EnabledQuerySubscriptionOptions` (where `query` and `token` are required) and `DisabledQuerySubscriptionOptions` (where they are not), so a disabled subscription type-checks with `initialData` alone.

This is what lets a single component render both the static and the live version of a page, toggling one boolean.

## Connection status

The `status` property represents the state of the server-sent events connection. It can be one of the following:

- `connecting`: the subscription channel is trying to connect
- `connected`: the channel is open, we're receiving live updates
- `closed`: the channel is not open. Either it has been permanently closed due to a fatal error (ie. an invalid query), or the subscription was never started because `enabled` is `false`

## Error object

| prop     | type   | description                                             |
| -------- | ------ | ------------------------------------------------------- |
| code     | string | The code of the error (ie. `INVALID_QUERY`)             |
| message  | string | An human friendly message explaining the error          |
| response | Object | The raw response returned by the endpoint, if available |

## Example

```svelte
<script>
  import { querySubscription } from '@datocms/svelte';

  const subscription = querySubscription({
    enabled: true,
    query: `
      query AppQuery($first: IntType) {
        allBlogPosts(first: $first) {
          slug
          title
        }
      }`,
    variables: { first: 10 },
    token: 'YOUR_API_TOKEN',
  });

  $: ({ data, error, status } = $subscription);

  const statusMessage = {
    connecting: 'Connecting to DatoCMS...',
    connected: 'Connected to DatoCMS, receiving live updates!',
    closed: 'Connection closed',
  };
</script>

<p>Connection status: {statusMessage[status]}</p>

{#if error}
  <h1>Error: {error.code}</h1>
  <p>{error.message}</p>
  {#if error.response}
    <pre>{JSON.stringify(error.response, null, 2)}</pre>
  {/if}
{/if}

{#if data}
  <ul>
    {#each data.allBlogPosts as blogPost (blogPost.slug)}
      <li>{blogPost.title}</li>
    {/each}
  </ul>
{/if}
```

For a complete, production-style setup, see the [DatoCMS SvelteKit Starter Kit](https://github.com/datocms/sveltekit-starter-kit), which wires `querySubscription` into a draft-mode / [Visual Editing](https://www.datocms.com/docs/visual-editing) flow:

- **[`src/lib/datocms/queries.ts`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/lib/datocms/queries.ts)** — the `generateRealtimeSubscription()` helper returns the `QuerySubscriptionOptions` object this store consumes: it fetches `initialData` with `executeQuery`, switches between the published and draft CDA tokens, and only sets `enabled: true` (with `contentLink: 'v1'`) when Draft Mode is active.
- **[`src/routes/page/[slug]/+page.server.ts`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/routes/page/%5Bslug%5D/+page.server.ts)** — the `load` function composes the typed GraphQL query and returns `generateRealtimeSubscription(event, query, { slug })` as `data.subscription`.
- **[`src/routes/page/[slug]/+page.svelte`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/routes/page/%5Bslug%5D/+page.svelte)** — the component calls `querySubscription(data.subscription)` and renders from `$subscription.data` (the site-wide [`+layout.svelte`](https://github.com/datocms/sveltekit-starter-kit/blob/main/src/routes/+layout.svelte) uses the same pattern).

---

# Svelte — <ContentLink> component for Visual Editing

Source [github]: https://raw.githubusercontent.com/datocms/datocms-svelte/main/src/lib/components/ContentLink/README.md

`<ContentLink />` is a Svelte component that enables **Visual Editing** for your DatoCMS content. It provides click-to-edit overlays that allow editors to click on any content element on your website to instantly open the DatoCMS editor and modify that specific field.

This component is built on top of the [`@datocms/content-link`](https://www.npmjs.com/package/@datocms/content-link) library and provides a seamless integration for Svelte and SvelteKit projects.

## What is Visual Editing?

Visual Editing transforms how content editors interact with your website. Instead of navigating through forms and fields in a CMS, editors can:

1. **See their content in context** - Preview exactly how content appears on the live site
2. **Click to edit** - Click directly on any text, image, or field to open the editor
3. **Navigate seamlessly** - Jump between pages in the preview, and the CMS follows along
4. **Get instant feedback** - Changes in the CMS are reflected immediately in the preview

This drastically improves the editing experience, especially for non-technical users who can now edit content without understanding the underlying CMS structure.

## Out-of-the-box features

- **Click-to-edit overlays**: Visual indicators showing which content is editable
- **Stega decoding**: Automatically detects and decodes editing metadata embedded in content
- **Keyboard shortcuts**: Hold Alt/Option to temporarily enable editing mode
- **Flash-all highlighting**: Show all editable areas at once for quick orientation
- **Bidirectional navigation**: Sync navigation between preview and DatoCMS editor
- **Framework-agnostic**: Works with SvelteKit or any routing solution
- **StructuredText integration**: Special support for complex structured content fields
- **[Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) integration**: Seamless integration with DatoCMS's editing interface

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->

- [Installation](#installation)
- [Basic Setup](#basic-setup)
  - [1. Fetch content with stega encoding](#1-fetch-content-with-stega-encoding)
  - [2. Add ContentLink component to your app](#2-add-contentlink-component-to-your-app)
- [SvelteKit integration](#sveltekit-integration)
- [Enabling click-to-edit](#enabling-click-to-edit)
- [Flash-all highlighting](#flash-all-highlighting)
- [Props](#props)
  - [ClickToEditOptions](#clicktoeditoptions)
- [Data attributes reference](#data-attributes-reference)
  - [Developer-specified attributes](#developer-specified-attributes)
    - [`data-datocms-content-link-url`](#data-datocms-content-link-url)
    - [`data-datocms-content-link-source`](#data-datocms-content-link-source)
    - [`data-datocms-content-link-group`](#data-datocms-content-link-group)
    - [`data-datocms-content-link-boundary`](#data-datocms-content-link-boundary)
  - [Library-managed attributes](#library-managed-attributes)
    - [`data-datocms-contains-stega`](#data-datocms-contains-stega)
    - [`data-datocms-auto-content-link-url`](#data-datocms-auto-content-link-url)
- [How group and boundary resolution works](#how-group-and-boundary-resolution-works)
- [Structured Text fields](#structured-text-fields)
  - [Rule 1: Always wrap the Structured Text component in a group](#rule-1-always-wrap-the-structured-text-component-in-a-group)
  - [Rule 2: Wrap embedded blocks, inline blocks, and inline records in a boundary](#rule-2-wrap-embedded-blocks-inline-blocks-and-inline-records-in-a-boundary)
- [Low-level utilities](#low-level-utilities)
  - [`decodeStega`](#decodestega)
  - [`stripStega`](#stripstega)
- [Troubleshooting](#troubleshooting)
  - [Click-to-edit overlays not appearing](#click-to-edit-overlays-not-appearing)
  - [Navigation not syncing with Web Previews plugin](#navigation-not-syncing-with-web-previews-plugin)
  - [StructuredText blocks not clickable](#structuredtext-blocks-not-clickable)
  - [Layout issues caused by stega encoding](#layout-issues-caused-by-stega-encoding)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Installation

```bash
npm install --save @datocms/svelte
```

The package includes `@datocms/content-link` as a dependency, which provides the underlying controller for Visual Editing functionality.

## Basic Setup

Visual Editing requires two steps:

### 1. Fetch content with stega encoding

When fetching content from DatoCMS, enable stega encoding to embed editing metadata:

```js
import { executeQuery } from '@datocms/cda-client';

const query = `
  query {
    page {
      title
      content
    }
  }
`;

const result = await executeQuery(query, {
  token: 'YOUR_API_TOKEN',
  environment: 'main',
  // Enable stega encoding
  contentLink: 'v1',
  // Set your site's base URL for editing links
  baseEditingUrl: 'https://your-project.admin.datocms.com',
});
```

The `contentLink: 'v1'` option enables stega encoding, which embeds invisible metadata into text fields. The `baseEditingUrl` tells DatoCMS where your project is located so edit URLs can be generated correctly. Both options are required.

### 2. Add ContentLink component to your app

Add the `<ContentLink />` component to your app. It doesn't render any visible output but sets up the click-to-edit functionality:

```svelte
<script>
  import { ContentLink } from '@datocms/svelte';
</script>

<ContentLink />

<!-- Your content here -->
```

That's it! The component will automatically detect editable content and create interactive overlays.

## SvelteKit integration

For SvelteKit projects, you can integrate with the routing system to enable full Web Previews plugin support:

```svelte
<script>
  import { ContentLink } from '@datocms/svelte';
  import { goto } from '$app/navigation';
  import { page } from '$app/stores';
</script>

<ContentLink
  onNavigateTo={(path) => goto(path)}
  currentPath={$page.url.pathname}
/>

<!-- Your content here -->
```

This integration enables:
- **Navigation from plugin**: When editors navigate to a different URL in the Visual Editing mode, your preview updates accordingly
- **Current path sync**: The plugin knows which page is currently being previewed

## Enabling click-to-edit

Click-to-edit overlays are **not enabled by default**. Instead, editors can:

- **Hold Alt/Option key**: Temporarily enable click-to-edit mode while the key is held down
- **Release the key**: Disable click-to-edit mode when released

If you prefer to enable click-to-edit programmatically on mount, set the `enableClickToEdit` prop:

```svelte
<ContentLink enableClickToEdit={true} />
```

Or with options:

```svelte
<ContentLink enableClickToEdit={{ scrollToNearestTarget: true }} />
<ContentLink enableClickToEdit={{ hoverOnly: true }} />
<ContentLink enableClickToEdit={{ hoverOnly: true, scrollToNearestTarget: true }} />
```

The `hoverOnly` option is useful to avoid showing overlays on touch devices where they may interfere with normal scrolling and tapping behavior. When set to `true` on a touch-only device, click-to-edit will not be automatically enabled, but users can still toggle it manually using the Alt/Option key.

## Flash-all highlighting

The flash-all feature provides visual feedback by highlighting all editable elements on the page. This is useful for:
- Showing editors what content they can edit
- Debugging to verify Visual Editing is working correctly
- Onboarding new content editors

When you enable click-to-edit with the `scrollToNearestTarget` option, it triggers the flash-all effect:

```svelte
<ContentLink enableClickToEdit={{ scrollToNearestTarget: true }} />
```

The `scrollToNearestTarget` parameter scrolls to the nearest editable element, useful on long pages.

## Props

| Prop                | Type                            | Default | Description                                                                                                                                            |
| ------------------- | ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `onNavigateTo`      | `(path: string) => void`        | -       | Callback when [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews) requests navigation to a different page |
| `currentPath`       | `string`                        | -       | Current pathname to sync with [Web Previews plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews)                         |
| `enableClickToEdit` | `boolean \| ClickToEditOptions` | -       | Enable click-to-edit overlays on mount. Pass `true` or an object with options. If undefined or false, click-to-edit is disabled                        |
| `stripStega`        | `boolean`                       | -       | Whether to strip stega encoding from text nodes after stamping                                                                                         |
| `root`              | `ParentNode`                    | -       | Root element to limit scanning to instead of the entire document                                                                                       |
| `hue`               | `number`                        | `17`    | Hue (0–359) of the overlay accent color. Default is the DatoCMS hue (`17`). Use this to match your brand or project colors                             |

### ClickToEditOptions

When passing an object to `enableClickToEdit`, the following options are available:

| Option                  | Type      | Default | Description                                                                                                                                                                                                    |
| ----------------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `scrollToNearestTarget` | `boolean` | `false` | Automatically scroll to the nearest editable element if none is currently visible in the viewport when click-to-edit mode is enabled. Also triggers the flash-all highlighting effect.                         |
| `hoverOnly`             | `boolean` | `false` | Only enable click-to-edit on devices that support hover (non-touch devices). Uses `window.matchMedia('(hover: hover)')` to detect hover capability. Useful to avoid overlays interfering with touch scrolling. |

## Data attributes reference

This library uses several `data-datocms-*` attributes. Some are **developer-specified** (you add them to your markup), and some are **library-managed** (added automatically during DOM stamping). Here's a complete reference.

### Developer-specified attributes

These attributes are added by you in your templates/components to control how editable regions behave.

#### `data-datocms-content-link-url`

Manually marks an element as editable with an explicit edit URL. Use this for non-text fields (booleans, numbers, dates, JSON) that cannot contain stega encoding. The recommended approach is to use the `_editingUrl` field available on all records:

```graphql
query {
  product {
    id
    price
    isActive
    _editingUrl
  }
}
```

```svelte
<span data-datocms-content-link-url={product._editingUrl}>
  ${product.price}
</span>
```

#### `data-datocms-content-link-source`

Attaches stega-encoded metadata without the need to render it as content. Useful for structural elements that cannot contain text (like `<video>`, `<audio>`, `<iframe>`, etc.) or when stega encoding in visible text would be problematic:

```svelte
<div data-datocms-content-link-source={video.alt}>
  <video src={video.url} poster={video.posterImage.url} controls />
</div>
```

The value must be a stega-encoded string (any text field from the API will work). The library decodes the stega metadata from the attribute value and makes the element clickable to edit.

#### `data-datocms-content-link-group`

Expands the clickable area to a parent element. When the library encounters stega-encoded content, by default it makes the immediate parent of the text node clickable to edit. Adding this attribute to an ancestor makes that ancestor the clickable target instead:

```svelte
<article data-datocms-content-link-group>
  <!-- product.title contains stega encoding -->
  <h2>{product.title}</h2>
  <p>${product.price}</p>
</article>
```

Here, clicking anywhere in the `<article>` opens the editor, rather than requiring users to click precisely on the `<h2>`.

**Important:** A group should contain only one stega-encoded source. If multiple stega strings resolve to the same group, the library logs a collision warning and only the last URL wins.

#### `data-datocms-content-link-boundary`

Stops the upward DOM traversal that looks for a `data-datocms-content-link-group`, making the element where stega was found the clickable target instead. This creates an independent editable region that won't merge into a parent group (see [How group and boundary resolution works](#how-group-and-boundary-resolution-works) below for details):

```svelte
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <section data-datocms-content-link-boundary>
    <!-- page.author contains stega encoding → resolves to URL B -->
    <span>{page.author}</span>
  </section>
</div>
```

Without the boundary, clicking `page.author` would open URL A (the outer group). With the boundary, the `<span>` becomes the clickable target opening URL B.

The boundary can also be placed directly on the element that contains the stega text:

```svelte
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <!-- page.author contains stega encoding → resolves to URL B -->
  <span data-datocms-content-link-boundary>{page.author}</span>
</div>
```

Here, the `<span>` has the boundary and directly contains the stega text, so the `<span>` itself becomes the clickable target (since the starting element and the boundary element are the same).

### Library-managed attributes

These attributes are added automatically by the library during DOM stamping. You do not need to add them yourself, but you can target them in CSS or JavaScript.

#### `data-datocms-contains-stega`

Added to elements whose text content contains stega-encoded invisible characters. This attribute is only present when `stripStega` is `false` (the default), since with `stripStega: true` the characters are removed entirely. Useful for CSS workarounds — the zero-width characters can sometimes cause unexpected letter-spacing or text overflow:

```css
[data-datocms-contains-stega] {
  letter-spacing: 0 !important;
}
```

#### `data-datocms-auto-content-link-url`

Added automatically to elements that the library has identified as editable targets (through stega decoding and group/boundary resolution). Contains the resolved edit URL.

This is the automatic counterpart to the developer-specified `data-datocms-content-link-url`. The library adds `data-datocms-auto-content-link-url` wherever it can extract an edit URL from stega encoding, while `data-datocms-content-link-url` is needed for non-text fields (booleans, numbers, dates, etc.) where stega encoding cannot be embedded. Both attributes are used by the click-to-edit overlay system to determine which elements are clickable and where they link to.

## How group and boundary resolution works

When the library encounters stega-encoded content inside an element, it walks up the DOM tree from that element:

1. If it finds a `data-datocms-content-link-group`, it stops and stamps **that** element as the clickable target.
2. If it finds a `data-datocms-content-link-boundary`, it stops and stamps the **starting element** as the clickable target — further traversal is prevented.
3. If it reaches the root without finding either, it stamps the **starting element**.

Here are some concrete examples to illustrate:

**Example 1: Nested groups**

```svelte
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <div data-datocms-content-link-group>
    <!-- page.subtitle contains stega encoding → resolves to URL B -->
    <p>{page.subtitle}</p>
  </div>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.subtitle`**: walks up from `<p>`, finds the inner group first → the **inner `<div>`** becomes clickable (opens URL B). The outer group is never reached.

Each nested group creates an independent clickable region. The innermost group always wins for its own content.

**Example 2: Boundary preventing group propagation**

```svelte
<div data-datocms-content-link-group>
  <!-- page.title contains stega encoding → resolves to URL A -->
  <h1>{page.title}</h1>
  <section data-datocms-content-link-boundary>
    <!-- page.author contains stega encoding → resolves to URL B -->
    <span>{page.author}</span>
  </section>
</div>
```

- **`page.title`**: walks up from `<h1>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.author`**: walks up from `<span>`, hits the `<section>` boundary → traversal stops, the **`<span>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 3: Boundary inside a group**

```svelte
<div data-datocms-content-link-group>
  <!-- page.description contains stega encoding → resolves to URL A -->
  <p>{page.description}</p>
  <div data-datocms-content-link-boundary>
    <!-- page.footnote contains stega encoding → resolves to URL B -->
    <p>{page.footnote}</p>
  </div>
</div>
```

- **`page.description`**: walks up from `<p>`, finds the outer group → the **outer `<div>`** becomes clickable (opens URL A).
- **`page.footnote`**: walks up from `<p>`, hits the boundary → traversal stops, the **`<p>`** itself becomes clickable (opens URL B). The outer group is not reached.

**Example 4: Multiple stega strings without groups (collision warning)**

```svelte
<p>
  <!-- Both product.name and product.tagline contain stega encoding -->
  {product.name}
  {product.tagline}
</p>
```

Both stega-encoded strings resolve to the same `<p>` element. The library logs a console warning and the last URL wins. To fix this, wrap each piece of content in its own element:

```svelte
<p>
  <span>{product.name}</span>
  <span>{product.tagline}</span>
</p>
```

## Structured Text fields

Structured Text fields require special attention because of how stega encoding works within them:

- The DatoCMS API encodes stega information inside a single `<span>` within the structured text output. Without any configuration, only that small span would be clickable.
- Structured Text fields can contain **embedded blocks** and **inline records**, each with their own editing URL that should open a different record in the editor.

Here are the rules to follow:

### Rule 1: Always wrap the Structured Text component in a group

This makes the entire structured text area clickable, instead of just the tiny stega-encoded span:

```svelte
<div data-datocms-content-link-group>
  <StructuredText data={page.content} />
</div>
```

### Rule 2: Wrap embedded blocks, inline blocks, and inline records in a boundary

Embedded blocks, inline blocks, and inline records each have their own edit URL (pointing to the block/record). Without a boundary, clicking them would bubble up to the parent group and open the structured text field editor instead. Add `data-datocms-content-link-boundary` to the root element of your custom components to prevent them from merging into the parent group.

**Note on record links (item links):** Record links (`isItemLink`) typically do **not** need a boundary. They render as `<a>` tags wrapping text that already belongs to the surrounding structured text. Unlike embedded blocks or inline records, record links don't introduce a separate editing target with its own stega-encoded URL, so there's no URL collision and no reason to isolate them from the parent group. When an editor clicks on that text, it correctly opens the structured text field editor (the parent group). Only add a boundary to a record link if you specifically want clicking it to open the linked record's editor instead.

```svelte
<script>
  import { StructuredText } from '@datocms/svelte';
  import { isBlock, isInlineBlock, isInlineItem, isItemLink } from 'datocms-structured-text-utils';
  import Block from './Block.svelte';
  import InlineBlock from './InlineBlock.svelte';
  import InlineItem from './InlineItem.svelte';
  import ItemLink from './ItemLink.svelte';
</script>

<div data-datocms-content-link-group>
  <StructuredText
    data={page.content}
    components={[
      [isBlock, Block],
      [isInlineBlock, InlineBlock],
      [isInlineItem, InlineItem],
      [isItemLink, ItemLink],
    ]}
  />
</div>
```

Then, in your custom components, wrap the root element with `data-datocms-content-link-boundary`:

```svelte
<!-- Block.svelte -->
<script>
  export let block;
</script>

<div data-datocms-content-link-boundary>
  <h2>{block.title}</h2>
  <p>{block.description}</p>
</div>
```

```svelte
<!-- InlineBlock.svelte -->
<script>
  export let block;
</script>

<span data-datocms-content-link-boundary>
  <em>{block.username}</em>
</span>
```

```svelte
<!-- InlineItem.svelte -->
<script>
  export let link;
</script>

<span data-datocms-content-link-boundary>
  {link.title}
</span>
```

Record links don't need a boundary — their content belongs to the surrounding structured text:

```svelte
<!-- ItemLink.svelte -->
<script>
  export let link;
</script>

<a href={`/posts/${link.slug}`}>
  <slot />
</a>
```

With this setup:
- Clicking the main text (paragraphs, headings, lists) — including record links — opens the **structured text field editor**
- Clicking an embedded block, inline block, or inline record opens **that record's editor**

## Low-level utilities

The `@datocms/svelte` package re-exports utility functions from `@datocms/content-link` for working with stega-encoded content:

### `decodeStega`

Decodes stega-encoded content to extract editing metadata:

```typescript
import { decodeStega } from '@datocms/svelte';

const text = "Hello, world!"; // Contains invisible stega data
const decoded = decodeStega(text);

if (decoded) {
  console.log('Editing URL:', decoded.url);
  console.log('Clean text:', decoded.cleanText);
}
```

### `stripStega`

Removes stega encoding from any data type:

```typescript
import { stripStega } from '@datocms/svelte';

// Works with strings
stripStega("Hello‎World") // "HelloWorld"

// Works with objects
stripStega({ name: "John‎", age: 30 })

// Works with nested structures - removes ALL stega encodings
stripStega({
  users: [
    { name: "Alice‎", email: "alice‎.com" },
    { name: "Bob‎", email: "bob‎.co" }
  ]
})

// Works with arrays
stripStega(["First‎", "Second‎", "Third‎"])
```

These utilities are useful when you need to:
- Extract clean text for meta tags or social sharing
- Check if content has stega encoding
- Debug Visual Editing issues
- Process stega-encoded content programmatically

## Troubleshooting

### Click-to-edit overlays not appearing

**Problem**: Overlays don't appear when clicking on content.

**Solutions**:
1. Verify stega encoding is enabled in your API calls:
   ```js
   const result = await executeQuery(query, {
     token: 'YOUR_API_TOKEN',
     contentLink: 'v1',
     baseEditingUrl: 'https://your-project.admin.datocms.com',
   });
   ```

2. Check that `<ContentLink />` is mounted in your component tree

3. Ensure you've enabled click-to-edit mode:
   ```svelte
   <ContentLink enableClickToEdit={true} />
   ```
   Or hold Alt/Option key while browsing

4. Check browser console for errors

### Navigation not syncing with Web Previews plugin

**Problem**: When you navigate in your preview, the DatoCMS editor doesn't follow along.

**Solutions**:
1. Ensure you're providing both `onNavigateTo` and `currentPath` props:
   ```svelte
   <ContentLink
     onNavigateTo={(path) => goto(path)}
     currentPath={$page.url.pathname}
   />
   ```

2. Verify `currentPath` updates when navigation occurs

3. Check that `baseEditingUrl` in your API calls matches your preview URL

### StructuredText blocks not clickable

**Problem**: Content within StructuredText blocks doesn't have click-to-edit overlays.

**Solutions**:
1. Wrap StructuredText with `data-datocms-content-link-group`:
   ```svelte
   <div data-datocms-content-link-group>
     <StructuredText data={content} />
   </div>
   ```

2. Add `data-datocms-content-link-boundary` to custom blocks, inline blocks, and inline records to prevent them from bubbling to the parent field (record links typically don't need a boundary)

### Layout issues caused by stega encoding

**Problem**: The invisible zero-width characters can cause unexpected letter-spacing or text breaking out of containers.

**Solutions**:
1. Use the `stripStega` prop to remove stega encoding after processing:
   ```svelte
   <ContentLink stripStega={true} />
   ```

2. Use CSS to reset letter-spacing on elements with stega-encoded content:
   ```css
   [data-datocms-contains-stega] {
     letter-spacing: 0 !important;
   }
   ```
   This attribute is automatically added to elements with stega-encoded content when `stripStega: false` (the default)

---

# structured-text-utils — Shared utilities and TypeScript types for Structured Text `dast`

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/utils/README.md

A set of Typescript types and helpers to work with DatoCMS Structured Text fields.

## Installation

Using [npm](http://npmjs.org/):

```sh
npm install datocms-structured-text-utils
```

Using [yarn](https://yarnpkg.com/):

```sh
yarn add datocms-structured-text-utils
```

## `dast` document validation

You can use the `validate()` function to check if an object is compatible with the [`dast` specification](https://www.datocms.com/docs/structured-text/dast):

```js
import { validate } from 'datocms-structured-text-utils';

const structuredText = {
  value: {
    schema: 'dast',
    document: {
      type: 'root',
      children: [
        {
          type: 'heading',
          level: 1,
          children: [
            {
              type: 'span',
              value: 'Hello!',
              marks: ['invalidmark'],
            },
          ],
        },
      ],
    },
  },
};

const result = validate(structuredText);

if (!result.valid) {
  console.error(result.message); // "span has an invalid mark "invalidmark"
}
```

## `dast` format specs

The package exports a number of constants that represents the rules of the [`dast` specification](https://www.datocms.com/docs/structured-text/dast).

Take a look a the [definitions.ts](https://github.com/datocms/structured-text/blob/main/packages/utils/src/definitions.ts) file for their definition:

```javascript
const blockquoteNodeType = 'blockquote';
const blockNodeType = 'block';
const codeNodeType = 'code';
const headingNodeType = 'heading';
const inlineItemNodeType = 'inlineItem';
const itemLinkNodeType = 'itemLink';
const linkNodeType = 'link';
const listItemNodeType = 'listItem';
const listNodeType = 'list';
const paragraphNodeType = 'paragraph';
const rootNodeType = 'root';
const spanNodeType = 'span';

const allowedNodeTypes = [
  'paragraph',
  'list',
  // ...
];

const allowedChildren = {
  paragraph: 'inlineNodes',
  list: ['listItem'],
  // ...
};

const inlineNodeTypes = [
  'span',
  'link',
  // ...
];

const allowedAttributes = {
  heading: ['level', 'children'],
  // ...
};

const allowedMarks = [
  'strong',
  'code',
  // ...
];
```

## Typescript Types

The package exports Typescript types for all the different nodes that a [`dast` document](https://www.datocms.com/docs/structured-text/dast) can contain.

Take a look a the [types.ts](https://github.com/datocms/structured-text/blob/main/packages/utils/src/types.ts) file for their definition:

```typescript
type Node
type BlockNode
type InlineNode
type RootType
type Root
type ParagraphType
type Paragraph
type HeadingType
type Heading
type ListType
type List
type ListItemType
type ListItem
type CodeType
type Code
type BlockquoteType
type Blockquote
type BlockType
type Block
type SpanType
type Mark
type Span
type LinkType
type Link
type ItemLinkType
type ItemLink
type InlineItemType
type InlineItem
type WithChildrenNode
type Document
type NodeType
type CdaStructuredTextValue
type Record
```

## Typescript Type guards

It also exports all a number of [type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) that you can use to guarantees the type of a node in some scope.

Take a look a the [guards.ts](https://github.com/datocms/structured-text/blob/main/packages/utils/src/guards.ts) file for their definition:

```typescript
function hasChildren(node: Node): node is WithChildrenNode {}
function isInlineNode(node: Node): node is InlineNode {}
function isHeading(node: Node): node is Heading {}
function isSpan(node: Node): node is Span {}
function isRoot(node: Node): node is Root {}
function isParagraph(node: Node): node is Paragraph {}
function isList(node: Node): node is List {}
function isListItem(node: Node): node is ListItem {}
function isBlockquote(node: Node): node is Blockquote {}
function isBlock(node: Node): node is Block {}
function isCode(node: Node): node is Code {}
function isLink(node: Node): node is Link {}
function isItemLink(node: Node): node is ItemLink {}
function isInlineItem(node: Node): node is InlineItem {}
function isCdaStructuredTextValue(
  object: any,
): object is CdaStructuredTextValue {}
```

### Narrowing blocks by model

When your DAST tree has been fetched with typed responses (eg. the CMA client in `nested: true` mode), `block.item` / `inlineBlock.item` is a union of all possible block-model shapes. `isBlockWithItemOfType` and `isInlineBlockWithItemOfType` filter that union down to a single model and narrow the node accordingly.

Both guards support two call styles:

- Curried — `isBlockWithItemOfType(itemTypeId)` returns a predicate, handy with `findFirstNode` / `findAllNodes` / `Array#filter`.
- Direct — `isBlockWithItemOfType(itemTypeId, node)` checks a node inline (e.g. inside an `if`).

```typescript
import {
  findFirstNode,
  isBlockWithItemOfType,
  isInlineBlockWithItemOfType,
} from 'datocms-structured-text-utils';

const WARNING_BLOCK_TYPE_ID = 'abc123' as const;
const CALLOUT_BLOCK_TYPE_ID = 'def456' as const;

// Curried — block
const needle = findFirstNode(
  body.document,
  isBlockWithItemOfType(WARNING_BLOCK_TYPE_ID),
);

if (needle) {
  // needle.node.item is narrowed to the Warning block-model shape
  console.log(needle.node.item.attributes.message);
}

// Direct — block
if (isBlockWithItemOfType(WARNING_BLOCK_TYPE_ID, node)) {
  console.log(node.item.attributes.message);
}

// Same shape for inline blocks
const callout = findFirstNode(
  body.document,
  isInlineBlockWithItemOfType(CALLOUT_BLOCK_TYPE_ID),
);

if (callout) {
  // callout.node.item is narrowed to the Callout inline-block-model shape
  console.log(callout.node.item.attributes.label);
}

if (isInlineBlockWithItemOfType(CALLOUT_BLOCK_TYPE_ID, node)) {
  console.log(node.item.attributes.label);
}
```

Pass the `itemTypeId` as a literal (`as const` on pre-set constants) for narrowing to kick in. At runtime the guards walk `item.relationships.item_type.data.id`, so they work for any block item carrying that shape — CMA nested-mode responses and the object variants of request payloads. Bare string IDs (used in request payloads to reference unchanged blocks) are filtered out.

## Tree Manipulation Utilities

The package provides a comprehensive set of utilities for traversing, transforming, and querying structured text trees. All utilities support both synchronous and asynchronous operations, work with both document wrappers and plain nodes, and provide full TypeScript support with proper type narrowing.

### Visiting Nodes

| Function                                                                                                           | Description                                                           |
| ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| [`forEachNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L111)      | Visit every node in the tree synchronously using pre-order traversal  |
| [`forEachNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L145) | Visit every node in the tree asynchronously using pre-order traversal |

Visit all nodes in the tree using pre-order traversal:

```javascript
import { forEachNode, forEachNodeAsync } from 'datocms-structured-text-utils';

// Synchronous traversal
forEachNode(structuredText, (node, parent, path) => {
  console.log(`Node type: ${node.type}, Path: ${path.join('.')}`);
});

// Asynchronous traversal
await forEachNodeAsync(structuredText, async (node, parent, path) => {
  await processNode(node);
});
```

### Transforming Trees

| Function                                                                                                        | Description                                                        |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| [`mapNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L309)      | Transform nodes in the tree synchronously (1:1, splat, or remove)  |
| [`mapNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L355) | Transform nodes in the tree asynchronously (1:1, splat, or remove) |

`mapNodes` walks the tree **bottom-up**: when the mapper sees a node, its
descendants have already been transformed, and the mapper's return for that
node is final.

The mapper may return:

- **a single node** — replaces the input node 1:1
- **an array of nodes** — splatted into the parent's children (1:N)
- **`null` or `undefined`** — removes the node from its parent (1:0)

Returning an array or nullish for the root node throws, since the function
returns a single node.

```javascript
import {
  mapNodes,
  mapNodesAsync,
  isHeading,
  isSpan,
  isThematicBreak,
} from 'datocms-structured-text-utils';

// 1:1 — transform heading levels for better hierarchy
const enhanced = mapNodes(structuredText, (node) => {
  if (isHeading(node) && node.level === 1) {
    return { ...node, level: 2 };
  }
  return node;
});

// 1:N — split a span into a span + a link by returning an array
const linked = mapNodes(structuredText, (node) => {
  if (!isSpan(node)) return node;
  const parts = node.value.split(/(\bclick here\b)/);
  return parts
    .filter((part) => part)
    .map((part) =>
      part === 'click here'
        ? {
            type: 'link',
            url: '/target',
            children: [{ type: 'span', value: 'click here' }],
          }
        : { type: 'span', value: part },
    );
});

// 1:0 — drop nodes by returning null
const compact = mapNodes(structuredText, (node) =>
  isThematicBreak(node) ? null : node,
);

// Async transformation with external API calls
const processed = await mapNodesAsync(structuredText, async (node) => {
  if (isSpan(node) && node.value.includes('TODO')) {
    const updatedText = await translateText(node.value);
    return { ...node, value: updatedText };
  }
  return node;
});
```

### Finding Nodes

| Function                                                                                                             | Description                                                  |
| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| [`collectNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L402)       | Collect all nodes that match a predicate function            |
| [`collectNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L460)  | Collect all nodes that match an async predicate function     |
| [`findFirstNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L499)      | Find the first node that matches a predicate function        |
| [`findFirstNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L577) | Find the first node that matches an async predicate function |

Find specific nodes using predicates or type guards:

```javascript
import {
  findFirstNode,
  findFirstNodeAsync,
  collectNodes,
  collectNodesAsync,
  isSpan,
  isHeading,
} from 'datocms-structured-text-utils';

// Find first node matching condition
const firstHeading = findFirstNode(structuredText, isHeading);
if (firstHeading) {
  console.log(`Found heading: ${firstHeading.node.level}`);
}

// Collect all nodes matching condition
const allSpans = collectNodes(structuredText, isSpan);
const textContent = allSpans.map(({ node }) => node.value).join('');

// Find nodes with specific attributes
const strongText = collectNodes(
  structuredText,
  (node) => isSpan(node) && node.marks?.includes('strong'),
);
```

### Filtering Trees

| Function                                                                                                           | Description                                             |
| ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| [`filterNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L626)      | Remove nodes that don't match a predicate synchronously |
| [`filterNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L709) | Remove nodes that don't match an async predicate        |

Remove nodes that don't match a predicate:

```javascript
import {
  filterNodes,
  filterNodesAsync,
  isCode,
  isBlock,
} from 'datocms-structured-text-utils';

// Remove all code blocks
const withoutCode = filterNodes(structuredText, (node) => !isCode(node));

// Async filtering with external validation
const validated = await filterNodesAsync(structuredText, async (node) => {
  if (isBlock(node)) {
    return await validateBlockItem(node.item);
  }
  return true;
});
```

### Reducing Trees

| Function                                                                                                           | Description                                                            |
| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| [`reduceNodes`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L796)      | Reduce the tree to a single value using a synchronous reducer function |
| [`reduceNodesAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L841) | Reduce the tree to a single value using an async reducer function      |

Reduce the entire tree to a single value:

```javascript
import { reduceNodes, reduceNodesAsync } from 'datocms-structured-text-utils';

// Extract all text content
const textContent = reduceNodes(
  structuredText,
  (acc, node) => {
    if (isSpan(node)) {
      return acc + node.value;
    }
    return acc;
  },
  '',
);

// Count nodes by type
const nodeCounts = reduceNodes(
  structuredText,
  (acc, node) => {
    acc[node.type] = (acc[node.type] || 0) + 1;
    return acc;
  },
  {},
);
```

### Checking Conditions

| Function                                                                                                         | Description                                                                           |
| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| [`someNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L883)       | Check if any node in the tree matches a predicate (short-circuit evaluation)          |
| [`someNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L925)  | Check if any node in the tree matches an async predicate (short-circuit evaluation)   |
| [`everyNode`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L967)      | Check if every node in the tree matches a predicate (short-circuit evaluation)        |
| [`everyNodeAsync`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/manipulation.ts#L998) | Check if every node in the tree matches an async predicate (short-circuit evaluation) |

Test if any or all nodes match a condition:

```javascript
import {
  someNode,
  everyNode,
  someNodeAsync,
  everyNodeAsync,
  isHeading,
  isSpan,
  isBlock,
} from 'datocms-structured-text-utils';

// Check if document contains any headings
const hasHeadings = someNode(structuredText, isHeading);

// Check if all spans have text content
const allSpansHaveText = everyNode(
  structuredText,
  (node) => !isSpan(node) || (node.value && node.value.length > 0),
);

// Async validation
const allBlocksValid = await everyNodeAsync(
  structuredText,
  async (node) => !isBlock(node) || (await validateBlock(node.item)),
);
```

### Type Safety and Path Information

All utilities provide full TypeScript support with type narrowing and path information:

```typescript
// Type guards automatically narrow types
const headings = collectNodes(structuredText, isHeading);
// headings is now Array<{ node: Heading; path: TreePath }>

headings.forEach(({ node, path }) => {
  // TypeScript knows node is Heading type
  console.log(`Level ${node.level} heading at ${path.join('.')}`);
});

// Custom type guards work too
const strongSpans = collectNodes(
  structuredText,
  (node): node is Span => isSpan(node) && node.marks?.includes('strong'),
);
// strongSpans is now Array<{ node: Span; path: TreePath }>
```

## Tree Visualization with Inspector

The package includes a powerful tree visualization utility that renders structured text documents as ASCII trees, making it easy to debug and understand document structure during development.

### Basic Usage

| Function                                                                                               | Description                                                |
| ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- |
| [`inspect`](https://github.com/datocms/structured-text/blob/main/packages/utils/src/inspector.ts#L202) | Render a structured text document or node as an ASCII tree |

```javascript
import { inspect } from 'datocms-structured-text-utils';

const structuredText = {
  schema: 'dast',
  document: {
    type: 'root',
    children: [
      {
        type: 'heading',
        level: 1,
        children: [{ type: 'span', value: 'Main Title' }],
      },
      {
        type: 'paragraph',
        children: [
          { type: 'span', value: 'This is a ' },
          { type: 'span', marks: ['strong'], value: 'bold' },
          { type: 'span', value: ' paragraph.' },
        ],
      },
      {
        type: 'block',
        item: 'block-123',
      },
    ],
  },
};

console.log(inspect(structuredText));
```

**Output:**

```
├ heading (level: 1)
│ └ span "Main Title"
├ paragraph
│ ├ span "This is a "
│ ├ span (marks: strong) "bold"
│ └ span " paragraph."
└ block (item: "block-123")
```

### Custom Block Formatting

The inspector supports custom formatting for block and inline block nodes, allowing you to display rich information about embedded content:

```javascript
import { inspect } from 'datocms-structured-text-utils';

// Example with block objects instead of just IDs
const blockObject = {
  id: 'block-456',
  type: 'item',
  attributes: {
    title: 'Hero Section',
    subtitle: 'Welcome to our site',
    buttonText: 'Get Started',
  },
};

// Simple formatter
const tree = inspect(document, {
  blockFormatter: (item, maxWidth) => {
    if (typeof item === 'string') return `ID: ${item}`;
    return `id: ${item.id}\ntitle: ${item.attributes.title}`;
  },
});

console.log(tree);
```

**Output:**

```
├ paragraph
│ └ span "Content before block"
├ block
│ id: 456
│ title: Hero Section
└ paragraph
  └ span "Content after block"
```

---

# structured-text-dastdown — Lossless serialization between Structured Text `dast` and Markdown

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/dastdown/README.md

Lossless textual serialization for [DatoCMS Structured Text (`dast`)](https://www.datocms.com/docs/structured-text/dast) documents, with a parser and a serializer.

`dastdown` is a markdown-flavored format that round-trips through `dast` without losing information. It exists so you can do programmatic edits to structured text via plain string manipulation (search/replace, regex, diff/merge) instead of walking the AST.

The full grammar is documented in [`SPEC.md`](./SPEC.md).

## When to use it

Best for **text-heavy content** (articles, docs, book chapters) where edits are textual and may cross node boundaries: bulk find/replace, regex refactors, LLM rewrites, meaningful `git diff`s.

Not for landing pages made of opaque blocks. Referenced blocks stay opaque — `dastdown` only lets you move, duplicate, or remove them.

## Installation

```sh
npm install datocms-structured-text-dastdown
```

## At a glance

```js
import { parse, serialize } from 'datocms-structured-text-dastdown';

// 1. fetch the record and turn its structured text field into dastdown
const record = await client.items.find('article-id');
const text = serialize(record.body);

// → # Title
//
//   A paragraph about Acme Corp with **strong** text and a [link](https://example.com).
//
//   > A quote.
//   {attribution="Anon"}
//
//   <block id="1234"/>

// 2. edit as plain text — search/replace, regex, diff/merge, LLM rewrite, …
const edited = text
  .replace(/Acme Corp/g, '**Acme Inc.**') // rename + bold every occurrence
  .replace(/^# (.+)$/m, '# $1 (2026 edition)'); // tweak the H1

// 3. parse back to dast and push the update
await client.items.update('article-id', { body: parse(edited) });
```

### Round-trip with the original document

When you fetch a record with `?nested=true`, blocks come back as full item objects. Pass that document as the second argument to `parse` and the result keeps both the original static type and the original block items — `parse` just looks up each `<block id="…"/>` in the original by id and re-attaches the full object:

```ts
import { parse, serialize } from 'datocms-structured-text-dastdown';

const cur = await client.items.find<Schema.Article>('article-id', {
  nested: true,
});
const edited = serialize(cur.body).replace(/Acme Corp/g, '**Acme Inc.**');

const body = parse(edited, cur.body);
//    ^? StructuredTextFieldValueInNestedResponse<Schema.X, Schema.Y> | null
//    every untouched block/inlineBlock keeps its original `item` (same reference).

await client.items.update<Schema.Article>('article-id', { body });
```

This makes dastdown safe for prose-level edits even when blocks carry data the format cannot represent. If the edited text references an id that isn't in the original, `parse` throws — the signal that the block needs to be created via the regular CMA flow rather than through dastdown.

## Format cheat-sheet

| Construct       | Syntax                                    |
| --------------- | ----------------------------------------- |
| Heading         | `# H1` … `###### H6`                      |
| Paragraph style | `{style="lead"}` on the line after        |
| Bullet list     | `- item`                                  |
| Numbered list   | `1. item` (numbers are not semantic)      |
| Blockquote      | `> line` plus `{attribution="…"}` trailer |
| Code block      | ` ```lang ` … ` ``` ` (`{highlight=…}`)   |
| Thematic break  | `---`                                     |
| Block reference | `<block id="…"/>` (root-level only)       |
| Strong          | `**text**`                                |
| Emphasis        | `*text*`                                  |
| Code            | `` `text` ``                              |
| Strikethrough   | `~~text~~`                                |
| Highlight       | `==text==`                                |
| Underline       | `++text++`                                |
| Custom mark     | `<m k="footnote-ref">text</m>`            |
| Link            | `[label](url){meta="…"}`                  |
| Item link       | `[label](dato:item/123){meta="…"}`        |
| Inline item     | `<inlineItem id="…"/>`                    |
| Inline block    | `<inlineBlock id="…"/>`                   |
| Hard line break | `<br/>` inside a span                     |

Marks nest in canonical outer-to-inner order: `highlight → strikethrough → underline → strong → emphasis → code`, with custom marks innermost in alphabetical order.

## API

### `parse(input, original?)`

```ts
parse(input: string | null | undefined): Document | null;
parse<B, IB>(
  input: string | null | undefined,
  original: Document<B, IB> | null | undefined,
): Document<B, IB> | null;
```

Parses a `dastdown` source string into a `dast` document.

- `null` / `undefined` input → `null` (so the return type matches `StructuredTextFieldValue` from `@datocms/cma-client` exactly).
- `''` or whitespace-only string → a document with a single empty paragraph.
- Otherwise → the parsed document, validated against the `dast` schema.

By default, `block` / `inlineBlock` / `inlineItem` / `itemLink` references come back with their `item` field as a string id, since `dastdown` only encodes ids on the wire.

If a second argument is passed, each parsed `block` / `inlineBlock` is rehydrated by looking up its id in `original` and reusing the original `item` object. The return type follows `Document<B, IB>`, so a `parse(serialize(doc), doc)` round-trip preserves both static types and the original block items (e.g. full `BlockInNestedResponse<…>` objects from a `?nested=true` fetch). A serialized id that isn't present in `original` throws a `DastdownParseError`.

If the input is malformed, `parse` throws a `DastdownParseError` carrying `line` and `column` info:

```js
import { parse, DastdownParseError } from 'datocms-structured-text-dastdown';

try {
  parse('####### too many hashes');
} catch (err) {
  if (err instanceof DastdownParseError) {
    console.log(err.line, err.column, err.message);
  }
}
```

### `serialize(document)`

```ts
type SerializableBlockId = string | { id: string };

serialize<
  B extends SerializableBlockId = string,
  IB extends SerializableBlockId = string
>(document: Document<B, IB> | null | undefined): string
```

Serializes a `dast` document into a `dastdown` string.

- `null` / `undefined` → `''`.
- A document whose only content is an empty paragraph → `''` (so `serialize(parse(''))` round-trips).
- Any other invalid document → throws.

The signature accepts both the plain field-value shape (block items as string ids) and the `?nested=true` response shape (block items as full record-like objects). When `item` is an object, its `.id` is used.

```js
import { serialize } from 'datocms-structured-text-dastdown';

// works with plain ids:
serialize({
  schema: 'dast',
  document: {
    type: 'root',
    children: [{ type: 'block', item: 'abc-123' }],
  },
});
// → '<block id="abc-123"/>\n'

// works with nested-response items too — only `id` is read:
serialize({
  schema: 'dast',
  document: {
    type: 'root',
    children: [
      { type: 'block', item: { id: 'abc-123' /* ...rest of Item */ } },
    ],
  },
});
// → '<block id="abc-123"/>\n'
```

The request shape (where new blocks may not yet have an id) is intentionally **not** supported — there is no way to render a reference to a block that has no id.

### `canonicalize(document)`

```ts
canonicalize<B, IB>(document: Document<B, IB>): Document<B, IB>
```

Returns a structurally normalized copy of the document. It does not touch block items; only spans and marks are rewritten:

- Adjacent spans with identical mark sets are coalesced.
- Empty spans are dropped (except when removing them would leave a parent paragraph/heading/link with no children).
- Mark order is sorted into the canonical outer-to-inner sequence; custom marks are placed innermost in alphabetical order.

Round-trip property:

```js
import {
  parse,
  serialize,
  canonicalize,
} from 'datocms-structured-text-dastdown';

parse(serialize(d)); // ≡ canonicalize(d)
```

### `DastdownParseError`

Thrown by `parse` on malformed input. Exposes `line` (1-indexed) and `column` (1-indexed) properties.

## Round-trip semantics

| `text`               | `parse(text)`            | `serialize(parse(text))` |
| -------------------- | ------------------------ | ------------------------ |
| `null` / `undefined` | `null`                   | `''`                     |
| `''` / whitespace    | empty paragraph document | `''`                     |
| any valid `dastdown` | a `dast` document        | the same text            |

For two documents `d1` and `d2`:

- `parse(serialize(d)) ≡ canonicalize(d)` (block items collapse to string ids)
- `parse(serialize(d), d) ≡ canonicalize(d)` with original block items restored, type preserved
- `serialize(parse(text)) ≡ text` after one canonicalization pass

## Why not CommonMark?

`dastdown` extends markdown with constructs that vanilla CommonMark cannot represent: `==highlight==`, `++underline++`, `{attribute="trailer"}` on blocks, and self-closing XML tags for opaque references. It is not designed to be rendered by a generic markdown pipeline; for that, parse to `dast` and use one of the `to-html-string` / `to-dom-nodes` renderers.

---

# structured-text-to-plain-text — Render Structured Text `dast` to a plain-text string

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/to-plain-text/README.md

![Node.js CI](https://github.com/datocms/structured-text/workflows/Node.js%20CI/badge.svg)


Plain text renderer for the Structured Text document.

## Installation

Using [npm](http://npmjs.org/):

```sh
npm install datocms-structured-text-to-plain-text
```

Using [yarn](https://yarnpkg.com/):

```sh
yarn add datocms-structured-text-to-plain-text
```

## Usage

```javascript
import { render } from 'datocms-structured-text-to-plain-text';

const structuredText = {
  value: {
    schema: 'dast',
    document: {
      type: 'root',
      children: [
        {
          type: 'heading',
          level: 1,
          children: [
            {
              type: 'span',
              value: 'This\nis a\ntitle!',
            },
          ],
        },
      ],
    },
  },
};

render(structuredText); // -> "This is a title!"
```

You can also pass custom renderers for `itemLink`, `inlineItem`, `block` as optional parameters like so:

```javascript
import { render } from 'datocms-structured-text-to-plain-text';

const graphqlResponse = {
  value: {
    schema: 'dast',
    document: {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'span',
              value: 'A ',
            },
            {
              type: 'itemLink',
              item: '344312',
              children: [
                {
                  type: 'span',
                  value: 'record hyperlink',
                },
              ],
            },
            {
              type: 'span',
              value: ' and an inline record: ',
            },
            {
              type: 'inlineItem',
              item: '344312',
            },
          ],
        },
        {
          type: 'block',
          item: '812394',
        },
      ],
    },
  },
  blocks: [
    {
      id: '812394',
      image: { url: 'http://www.datocms-assets.com/1312/image.png' },
    },
  ],
  links: [{ id: '344312', title: 'Foo', slug: 'foo' }],
};

const options = {
  renderBlock({ record }) {
    return `[Image ${record.image.url}]`;
  },
  renderInlineRecord({ record, adapter: { renderNode } }) {
    return `[Inline ${record.slug}]${children}[/Inline]`;
  },
  renderLinkToRecord({ record, children, adapter: { renderNode } }) {
    return `[Link to ${record.slug}]${children}[/Link]`;
  },
};

render(document, options);
// -> A [Link to foo]record hyperlink[/Link] and an inline record: [Inline foo]Foo[/Inline]
//    [Image http://www.datocms-assets.com/1312/image.png]
```

---

# structured-text-to-markdown — Render Structured Text `dast` to Markdown

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/to-markdown/README.md

![Node.js CI](https://github.com/datocms/structured-text/workflows/Node.js%20CI/badge.svg)


Markdown renderer for the DatoCMS Structured Text field type.

## Installation

Using [npm](http://npmjs.org/):

```sh
npm install datocms-structured-text-to-markdown
```

Using [yarn](https://yarnpkg.com/):

```sh
yarn add datocms-structured-text-to-markdown
```

## Usage

```javascript
import { render } from 'datocms-structured-text-to-markdown';

render({
  schema: 'dast',
  document: {
    type: 'root',
    children: [
      {
        type: 'heading',
        level: 1,
        children: [
          {
            type: 'span',
            value: 'Hello world!',
          },
        ],
      },
      {
        type: 'paragraph',
        children: [
          {
            type: 'span',
            value: 'This is a paragraph.',
          },
        ],
      },
    ],
  },
});
// -> # Hello world!
//
//    This is a paragraph.
```

## Supported Markdown Features

The renderer supports all DatoCMS Structured Text nodes and converts them to CommonMark-compatible Markdown:

### Block Nodes

- **Headings**: `# H1` through `###### H6`
- **Paragraphs**: Plain text with double newlines
- **Lists**: Both bulleted (`-`) and numbered (`1.`) lists with nested support
- **Blockquotes**: Lines prefixed with `>`
- **Code blocks**: Fenced code blocks with language support
- **Thematic breaks**: Horizontal rules (`---`)

### Inline Formatting

- **Strong**: `**bold**`
- **Emphasis**: `*italic*`
- **Code**: `` `code` ``
- **Strikethrough**: `~~text~~`
- **Highlight**: `==text==` (extended Markdown)
- **Underline**: `<u>text</u>` (HTML fallback, no native Markdown)

### Links

- **Regular links**: `[text](url)`
- **Record links**: Custom rendering via `renderLinkToRecord`

## Behavior Notes

- **Escaping strategy**: `renderText` escapes `` \`*_{}[]()#+|<> `` to avoid accidental formatting or unintended HTML. For bespoke sanitization, supply a custom `renderText` implementation.
- **Ordered list markers**: Every numbered list item is rendered as `1.`. CommonMark parsers expand these into the correct numeric sequence automatically and this keeps the output stable even when items are reordered.
- **Blockquote attribution**: When a blockquote contains an `attribution` field, the renderer appends a final line formatted as `— Author`. This mirrors the DOM renderer's output but is not part of the Markdown core spec.

## Error Handling

The renderer surfaces meaningful `RenderError` instances when required data is missing:

- `inlineItem` nodes throw if you provide `renderInlineRecord` but the requested record is not present in `.links`. Without the handler, the node is skipped.
- `itemLink` nodes behave the same way: supplying `renderLinkToRecord` without the matching record raises, while omitting the handler falls back to the plain link text.
- `block` and `inlineBlock` nodes require both a renderer and a matching record. Missing renderers make the node render as empty; missing records raise.

Handle these errors upstream by passing the complete GraphQL response or adjusting your custom render callbacks.

## Advanced Usage

### Custom Rendering

You can pass custom renderers for nodes and text:

```javascript
import { render, renderNodeRule } from 'datocms-structured-text-to-markdown';
import { isHeading } from 'datocms-structured-text-utils';

const options = {
  renderText: (text) => text.toUpperCase(),
  customNodeRules: [
    renderNodeRule(
      isHeading,
      ({ node, children, adapter: { renderFragment } }) => {
        // Custom heading with decoration
        return renderFragment([
          `${'='.repeat(node.level)} `,
          ...(children || []),
          '\n\n',
        ]);
      },
    ),
  ],
};

render(document, options);
```

### Rendering DatoCMS records and blocks

You can pass custom renderers for `itemLink`, `inlineItem`, `block`, and `inlineBlock` nodes:

```javascript
import { render } from 'datocms-structured-text-to-markdown';

const graphqlResponse = {
  value: {
    schema: 'dast',
    document: {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'span',
              value: 'Check out ',
            },
            {
              type: 'itemLink',
              item: '123',
              children: [
                {
                  type: 'span',
                  value: 'this article',
                },
              ],
            },
            {
              type: 'span',
              value: ' and ',
            },
            {
              type: 'inlineItem',
              item: '123',
            },
            {
              type: 'span',
              value: '!',
            },
          ],
        },
        {
          type: 'block',
          item: '456',
        },
      ],
    },
  },
  blocks: [
    {
      id: '456',
      __typename: 'CalloutRecord',
      style: 'positive',
      title: '🛠️ Block and Structured Text utilities',
      content:
        'We provide many utility functions to help you work with blocks and structured text nodes effectively.',
    },
  ],
  links: [
    {
      id: '123',
      __typename: 'BlogPostRecord',
      title: 'My First Post',
      slug: 'my-first-post',
    },
  ],
};

const options = {
  renderInlineRecord: ({ record }) => {
    switch (record.__typename) {
      case 'BlogPostRecord':
        return `[${record.title}](/blog/${record.slug})`;
      default:
        return null;
    }
  },
  renderLinkToRecord: ({ record, children }) => {
    switch (record.__typename) {
      case 'BlogPostRecord':
        return `[${children}](/blog/${record.slug})`;
      default:
        return null;
    }
  },
  renderBlock: ({ record }) => {
    switch (record.__typename) {
      case 'CalloutRecord': {
        // GitHub-flavored Markdown supports callout syntax
        const calloutType = record.style.toUpperCase();
        return `> [!${calloutType}] ${record.title}\n> ${record.content}\n\n`;
      }
      default:
        return null;
    }
  },
};

render(graphqlResponse, options);
// -> Check out [this article](/blog/my-first-post) and [My First Post](/blog/my-first-post)!
//
//    > [!POSITIVE] 🛠️ Block and Structured Text utilities
//    > We provide many utility functions to help you work with blocks and structured text nodes effectively.
```

## API

### `render(structuredText, options?)`

Converts a Structured Text document to a Markdown string.

#### Parameters

- `structuredText`: The Structured Text document (can be a full GraphQL response or a plain document)
- `options` (optional): Rendering options
  - `customNodeRules`: Array of custom node rendering rules
  - `customMarkRules`: Array of custom mark rendering rules
  - `renderInlineRecord`: Function to render `inlineItem` nodes
  - `renderLinkToRecord`: Function to render `itemLink` nodes
  - `renderBlock`: Function to render `block` nodes
  - `renderInlineBlock`: Function to render `inlineBlock` nodes
  - `renderText`: Function to customize text rendering
  - `renderNode`: Function to customize node rendering
  - `renderFragment`: Function to customize fragment rendering

#### Returns

A Markdown string, or `null` if the input is empty.

---

# structured-text-to-html-string — Render Structured Text `dast` to an HTML string

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/to-html-string/README.md

![Node.js CI](https://github.com/datocms/structured-text/workflows/Node.js%20CI/badge.svg)


HTML renderer for the DatoCMS Structured Text field type.

## Installation

Using [npm](http://npmjs.org/):

```sh
npm install datocms-structured-text-to-html-string
```

Using [yarn](https://yarnpkg.com/):

```sh
yarn add datocms-structured-text-to-html-string
```

## Usage

```javascript
import { render } from 'datocms-structured-text-to-html-string';

render({
  schema: 'dast',
  document: {
    type: 'root',
    children: [
      {
        type: 'paragraph',
        children: [
          {
            type: 'span',
            value: 'Hello world!',
          },
        ],
      },
    ],
  },
}); // -> <p>Hello world!</p>

render({
  type: 'root',
  children: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'span',
          value: 'Hello',
          marks: ['strong'],
        },
        {
          type: 'span',
          value: ' world!',
          marks: ['underline'],
        },
      ],
    },
  ],
}); // -> <p><strong>Hello</strong><u> world!</u></p>
```

You can pass custom renderers for nodes and text as optional parameters like so:

```javascript
import { render, renderNodeRule } from 'datocms-structured-text-to-html-string';
import { isHeading } from 'datocms-structured-text-utils';

const structuredText = {
  type: 'root',
  children: [
    {
      type: 'heading',
      level: 1,
      content: [
        {
          type: 'span',
          value: 'Hello world!',
        },
      ],
    },
  ],
};

const options = {
  renderText: (text) => text.replace(/Hello/, 'Howdy'),
  customNodeRules: [
    renderNodeRule(
      isHeading,
      ({ adapter: { renderNode }, node, children, key }) => {
        return renderNode(`h${node.level + 1}`, { key }, children);
      },
    ),
  ],
  customMarkRules: [
    renderMarkRule('strong', ({ adapter: { renderNode }, children, key }) => {
      return renderNode('b', { key }, children);
    }),
  ],
};

render(document, options);
// -> <h2>Howdy world!</h2>
```

Last, but not least, you can pass custom renderers for `itemLink`, `inlineItem`, `block` as optional parameters like so:

```javascript
import { render } from 'datocms-structured-text-to-html-string';

const graphqlResponse = {
  value: {
    schema: 'dast',
    document: {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'span',
              value: 'A ',
            },
            {
              type: 'itemLink',
              item: '344312',
              children: [
                {
                  type: 'span',
                  value: 'record hyperlink',
                },
              ],
            },
            {
              type: 'span',
              value: ' and an inline record: ',
            },
            {
              type: 'inlineItem',
              item: '344312',
            },
          ],
        },
        {
          type: 'block',
          item: '812394',
        },
      ],
    },
  },
  blocks: [
    {
      id: '812394',
      image: { url: 'http://www.datocms-assets.com/1312/image.png' },
    },
  ],
  links: [{ id: '344312', title: 'Foo', slug: 'foo' }],
};

const options = {
  renderBlock({ record, adapter: { renderNode } }) {
    return renderNode(
      'figure',
      {},
      renderNode('img', { src: record.image.url }),
    );
  },
  renderInlineRecord({ record, adapter: { renderNode } }) {
    return renderNode('a', { href: `/blog/${record.slug}` }, record.title);
  },
  renderLinkToRecord({ record, children, adapter: { renderNode } }) {
    return renderNode('a', { href: `/blog/${record.slug}` }, children);
  },
};

render(document, options);
// -> <p>A <a href="/blog/foo">record hyperlink</a> and an inline record: <a href="/blog/foo">Foo</a></p>
//    <figure><img src="http://www.datocms-assets.com/1312/image.png" /></figure>
```

---

# structured-text-to-dom-nodes — Render Structured Text `dast` to live DOM nodes

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/to-dom-nodes/README.md

![Node.js CI](https://github.com/datocms/structured-text/workflows/Node.js%20CI/badge.svg)


DOM nodes renderer for the DatoCMS Structured Text field type. To be used inside the browser, as it uses `document.createElement`.

## Installation

Using [npm](http://npmjs.org/):

```sh
npm install datocms-structured-text-to-dom-nodes
```

Using [yarn](https://yarnpkg.com/):

```sh
yarn add datocms-structured-text-to-dom-nodes
```

## Usage

```javascript
import { render } from 'datocms-structured-text-to-dom-nodes';

let nodes = render({
  schema: 'dast',
  document: {
    type: 'root',
    children: [
      {
        type: 'paragraph',
        children: [
          {
            type: 'span',
            value: 'Hello world!',
          },
        ],
      },
    ],
  },
});

console.log(nodes.map((node) => node.outerHTML)); // -> ["<p>Hello world!</p>"]

nodes = render({
  type: 'root',
  children: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'span',
          value: 'Hello',
          marks: ['strong'],
        },
        {
          type: 'span',
          value: ' world!',
          marks: ['underline'],
        },
      ],
    },
  ],
});

console.log(nodes.map((node) => node.outerHTML)); // -> ["<p><strong>Hello</strong><u> world!</u></p>"]
```

You can pass custom renderers for nodes and text as optional parameters like so:

```javascript
import { render, renderNodeRule } from 'datocms-structured-text-to-dom-nodes';
import { isHeading } from 'datocms-structured-text-utils';

const structuredText = {
  type: 'root',
  children: [
    {
      type: 'heading',
      level: 1,
      content: [
        {
          type: 'span',
          value: 'Hello world!',
        },
      ],
    },
  ],
};

const options = {
  renderText: (text) => text.replace(/Hello/, 'Howdy'),
  customNodeRules: [
    renderNodeRule(
      isHeading,
      ({ adapter: { renderNode }, node, children, key }) => {
        return renderNode(`h${node.level + 1}`, { key }, children);
      },
    ),
  ],
  customMarkRules: [
    renderMarkRule('strong', ({ adapter: { renderNode }, children, key }) => {
      return renderNode('b', { key }, children);
    }),
  ],
};

render(document, options);
// -> [<h2>Howdy world!</h2>]
```

Last, but not least, you can pass custom renderers for `itemLink`, `inlineItem`, `block` as optional parameters like so:

```javascript
import { render } from 'datocms-structured-text-to-dom-nodes';

const graphqlResponse = {
  value: {
    schema: 'dast',
    document: {
      type: 'root',
      children: [
        {
          type: 'paragraph',
          children: [
            {
              type: 'span',
              value: 'A ',
            },
            {
              type: 'itemLink',
              item: '344312',
              children: [
                {
                  type: 'span',
                  value: 'record hyperlink',
                },
              ],
            },
            {
              type: 'span',
              value: ' and an inline record: ',
            },
            {
              type: 'inlineItem',
              item: '344312',
            },
          ],
        },
        {
          type: 'block',
          item: '812394',
        },
      ],
    },
  },
  blocks: [
    {
      id: '812394',
      image: { url: 'http://www.datocms-assets.com/1312/image.png' },
    },
  ],
  links: [{ id: '344312', title: 'Foo', slug: 'foo' }],
};

const options = {
  renderBlock({ record, adapter: { renderNode } }) {
    return renderNode('figure', {}, renderNode('img', { src: record.url }));
  },
  renderInlineRecord({ record, adapter: { renderNode } }) {
    return renderNode('a', { href: `/blog/${record.slug}` }, record.title);
  },
  renderLinkToRecord({ record, children, adapter: { renderNode } }) {
    return renderNode('a', { href: `/blog/${record.slug}` }, children);
  },
};

render(document, options);
// -> [
//      <p>A <a href="/blog/foo">record hyperlink</a> and an inline record: <a href="/blog/foo">Foo</a></p>,
//      <figure><img src="http://www.datocms-assets.com/1312/image.png" /></figure>
//    ]
```

---

# html-to-structured-text — Convert HTML into a Structured Text `dast` document

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/html-to-structured-text/README.md

This package contains utilities to convert HTML (or a [Hast](https://github.com/syntax-tree/hast) tree) into a DatoCMS Structured Text `dast` (DatoCMS Abstract Syntax Tree) document.

Please refer to [the `dast` format docs](https://www.datocms.com/docs/structured-text/dast) to learn more about the syntax tree format and the available nodes.

## Requirements

Starting with v6, this package is **ESM-only** and requires **Node.js 18 or newer**. Use `import` (not `require()`) from native ESM, a bundler, or a TypeScript project with `module: "NodeNext"` (or equivalent).

If you need CommonJS support, pin to `^5.1.16`.

## Usage

The main utility in this package is `htmlToStructuredText` which takes a string of HTML and transforms it into a valid `dast` document.

`htmlToStructuredText` returns a `Promise` that resolves with a Structured Text document.

```js
import { htmlToStructuredText } from 'datocms-html-to-structured-text';

const html = `
  <article>
    <h1>DatoCMS</h1>
    <p>The most complete, user-friendly and performant Headless CMS.</p>
  </article>
`;

htmlToStructuredText(html).then((structuredText) => {
  console.log(structuredText);
});
```

`htmlToStructuredText` is meant to be used in a browser environment.

In Node.js you can use the `parse5ToStructuredText` helper which takes a document generated with `parse5`.

```js
import { parse } from 'parse5';
import { parse5ToStructuredText } from 'datocms-html-to-structured-text';

parse5ToStructuredText(
  parse(html, {
    sourceCodeLocationInfo: true,
  }),
).then((structuredText) => {
  console.log(structuredText);
});
```

Internally, both utilities work on a [Hast](https://github.com/syntax-tree/hast) tree. If you already have a `hast` tree, use `hastToStructuredText`:

```js
import { hastToStructuredText } from 'datocms-html-to-structured-text';

hastToStructuredText(hastTree).then((structuredText) => {
  console.log(structuredText);
});
```

## Validate `dast` documents

`dast` is a strict format for DatoCMS' Structured Text fields. As such the resulting document is generally a simplified, content-centric version of the input HTML.

When possible, the library relies on semantic HTML to generate a valid `dast` document.

The `datocms-structured-text-utils` package provides a `validate` utility to validate a value to make sure that the resulting tree is compatible with DatoCMS' Structured Text field.

```js
import { validate } from 'datocms-structured-text-utils';

// ...

htmlToStructuredText(html).then((structuredText) => {
  const { valid, message } = validate(structuredText);

  if (!valid) {
    throw new Error(message);
  }
});
```

We recommend validating every `dast` document to avoid errors later when creating records.

## Advanced Usage

### Options

All the `*ToStructuredText` utilities accept an optional `options` object as second argument:

```ts
import type { Root as HastRoot } from 'hast';

type Options = Partial<{
  newlines: boolean;
  // Override existing `hast` node handlers or add new ones
  handlers: Record<string, Handler>;
  // Allows to tweak the `hast` tree before transforming it to a `dast` document
  preprocess: (hast: HastRoot) => void;
  // Array of allowed block nodes
  allowedBlocks: Array<
    BlockquoteType | CodeType | HeadingType | LinkType | ListType
  >;
  // Array of allowed marks
  allowedMarks: Mark[];
  // Array of allowed heading levels for 'heading' nodes
  allowedHeadingLevels: Array<1 | 2 | 3 | 4 | 5 | 6>;
  // Properties shared across handler invocations via context.global
  shared: Record<string, unknown>;
}>;
```

### Transforming Nodes

The utilities in this library traverse a `hast` tree and transform supported nodes into `dast` nodes. The transformation is done by associating a handler (async) function to a `hast` node.

Handlers are associated to `hast` nodes by `tagName` or `type` (when `node.type !== 'element'`) and look like this:

```js
import { visitChildren } from 'datocms-html-to-structured-text';

// Handler for the <p> tag.
async function p(createDastNode, hastNode, context) {
  return createDastNode('paragraph', {
    children: await visitChildren(createDastNode, hastNode, context),
  });
}
```

Handlers can return either a promise that resolves to a `dast` node, an array of `dast` nodes or `undefined` to skip the current node.

To ensure that a valid `dast` is generated, the default handlers also check that the current `hastNode` is a valid `dast` node for its parent and, if not, they ignore the current node and continue visiting its children.

Information about the parent `dast` node name is available in `context.parentNodeType`.

Please take a look at the [default handlers implementation](./src/handlers.ts) for examples.

The default handlers are available on `context.defaultHandlers`.

### Context

Every handler receives a `context` object with the following shape:

```ts
import type { Nodes as HastNodes } from 'hast';

export interface GlobalContext {
  // Whether the library has found a <base> tag or should not look further.
  // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
  baseUrlFound?: boolean;
  // <base> tag url. Used for resolving relative URLs.
  baseUrl?: string | null;
  [key: string]: unknown;
}

export interface Context {
  // The current parent `dast` node type.
  parentNodeType: NodeType;
  // The parent `hast` node.
  parentNode: HastNodes | null;
  // A reference to the current handlers - merged default + user handlers.
  handlers: Record<string, Handler>;
  // A reference to the default handlers record (map).
  defaultHandlers: Record<string, Handler>;
  // true if the content can include newlines, and false if not (such as in headings).
  wrapText: boolean;
  // Marks for span nodes.
  marks?: Mark[];
  // Prefix for language detection in code blocks.
  // Detection is done on a class name eg class="language-html".
  // Default is `language-`.
  codePrefix?: string;
  // Allowed block types.
  allowedBlocks: string[];
  // Allowed heading levels.
  allowedHeadingLevels: Array<1 | 2 | 3 | 4 | 5 | 6>;
  // Allowed marks.
  allowedMarks: Mark[];
  // Properties in this object are available to every handler — Context
  // is not deeply cloned.
  global: GlobalContext;
}
```

`HastNodes` is the union of all hast node kinds (`Root | Element | Text | Comment | Doctype`) exported by [`@types/hast`](https://www.npmjs.com/package/@types/hast).

### Custom Handlers

It is possible to register custom handlers and override the default behavior via options:

```js
import { paragraphHandler } from './customHandlers.js';

htmlToStructuredText(html, {
  handlers: {
    p: paragraphHandler,
  },
}).then((structuredText) => {
  console.log(structuredText);
});
```

It is **highly encouraged** to validate the `dast` when using custom handlers because handlers are responsible for dictating valid parent-children relationships, and therefore for generating a tree that is compliant with DatoCMS' Structured Text.

## Preprocessing

Because of the strictness of the `dast` spec, some semantics or elements might be lost during transformation.

To improve the final result, you can modify the `hast` tree before it is transformed to `dast` via the `preprocess` hook.

```js
import { visit } from 'unist-util-visit';

const html = `
  <p>convert this to an h1</p>
`;

htmlToStructuredText(html, {
  preprocess: (tree) => {
    // Transform <p> to <h1>
    visit(tree, 'element', (node) => {
      if (node.tagName === 'p') {
        node.tagName = 'h1';
      }
    });
  },
}).then((structuredText) => {
  console.log(structuredText);
});
```

### Examples

<details>
  <summary>Split a node that contains an image.</summary>

In `dast` images can be represented as `Block` nodes, but these are not allowed inside `ListItem` nodes (ul/ol lists). In this example we split the list in three pieces and lift up the image.

The same approach can be used to split other types of branches and lift up nodes to become root nodes.

```js
import { visitParents } from 'unist-util-visit-parents';

const html = `
  <ul>
    <li>item 1</li>
    <li><div><img src="./img.png" alt></div></li>
    <li>item 2</li>
  </ul>
`;

const dast = await htmlToStructuredText(html, {
  preprocess: (tree) => {
    const liftedImages = new WeakSet();

    visitParents(tree, 'element', (node, ancestors) => {
      if (
        node.tagName !== 'img' ||
        liftedImages.has(node) ||
        ancestors.length <= 1 // already a top-level img
      ) {
        return;
      }

      const parents = ancestors;
      const imgParent = parents[parents.length - 1];
      const index = imgParent.children.indexOf(node);
      imgParent.children.splice(index, 1);

      let i = parents.length;
      let splitChildrenIndex = index;
      let childrenAfterSplitPoint = [];

      while (--i > 0) {
        const parent = parents[i];
        const parentsParent = parents[i - 1];

        // Delete the siblings after the image and save them.
        childrenAfterSplitPoint = parent.children.splice(splitChildrenIndex);

        splitChildrenIndex = parentsParent.children.indexOf(parent);

        let nodeInserted = false;

        // Once we reach the topmost parent, insert the image node.
        if (i === 1) {
          splitChildrenIndex += 1;
          parentsParent.children.splice(splitChildrenIndex, 0, node);
          liftedImages.add(node);
          nodeInserted = true;
        }

        splitChildrenIndex += 1;
        if (childrenAfterSplitPoint.length > 0) {
          parentsParent.children.splice(splitChildrenIndex, 0, {
            ...parent,
            children: childrenAfterSplitPoint,
          });
        }

        if (parent.children.length === 0) {
          splitChildrenIndex -= 1;
          parentsParent.children.splice(
            nodeInserted ? splitChildrenIndex - 1 : splitChildrenIndex,
            1,
          );
        }
      }
    });
  },
  handlers: {
    img: async (createNode, node, context) => {
      // In a real scenario you would upload the image to Dato and get back an id.
      const item = '123';
      return createNode('block', { item });
    },
  },
});
```

</details>

<details>
  <summary>Lift up an image node</summary>

```js
import { visitParents, CONTINUE } from 'unist-util-visit-parents';

const html = `
  <ul>
    <li>item 1</li>
    <li><div><img src="./img.png" alt>item 2</div></li>
    <li>item 3</li>
  </ul>
`;

const dast = await htmlToStructuredText(html, {
  preprocess: (tree) => {
    visitParents(tree, 'element', (node, ancestors) => {
      if (node.tagName === 'img' && ancestors.length > 1) {
        const parent = ancestors[ancestors.length - 1];
        const index = parent.children.indexOf(node);
        tree.children.push(node);
        parent.children.splice(index, 1);
        return [CONTINUE, index];
      }
    });
  },
  handlers: {
    img: async (createNode, node, context) => {
      // In a real scenario you would upload the image to Dato and get back an id.
      const item = '123';
      return createNode('block', { item });
    },
  },
});
```

</details>

### Utilities

To work with `hast` and `dast` trees we recommend the [unified ecosystem](https://unifiedjs.com/) — in particular:

- [`unist-util-visit`](https://www.npmjs.com/package/unist-util-visit) and [`unist-util-visit-parents`](https://www.npmjs.com/package/unist-util-visit-parents) for tree traversal
- [`@types/hast`](https://www.npmjs.com/package/@types/hast) for hast node types (`Root`, `Element`, `Text`, `Nodes`)

For `dast` trees specifically, the [`datocms-structured-text-utils`](https://www.npmjs.com/package/datocms-structured-text-utils) package provides tailored traversal helpers (`collectNodes`, `findFirstNode`, `mapNodes`, `filterNodes`, …).

## License

MIT

---

# structured-text-slate-utils — Convert between Structured Text `dast` and Slate.js editor state

Source [github]: https://raw.githubusercontent.com/datocms/structured-text/main/packages/slate-utils/README.md

A set of Typescript types and helpers to convert Structured Text dast to Slate structures.

## Installation

Using [npm](http://npmjs.org/):

```sh
npm install datocms-structured-text-slate-utils
```

Using [yarn](https://yarnpkg.com/):

```sh
yarn add datocms-structured-text-slate-utils
```

---

# datocms-listen — Framework-agnostic client for DatoCMS Real-Time Updates API

Source [github]: https://raw.githubusercontent.com/datocms/datocms-listen/main/README.md

![MIT](https://img.shields.io/npm/l/datocms-listen?style=for-the-badge) ![MIT](https://img.shields.io/npm/v/datocms-listen?style=for-the-badge) [![Build Status](https://img.shields.io/travis/datocms/datocms-listen?style=for-the-badge)](https://travis-ci.org/datocms/datocms-listen)

A lightweight, TypeScript-ready package that offers utilities to work with DatoCMS [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api) inside a browser.

## Installation

```
npm install datocms-listen
```

## Example

Import `subscribeToQuery` from `datocms-listen` and use it inside your components like this:

```js
import { subscribeToQuery } from "datocms-listen";

const unsubscribe = await subscribeToQuery({
  query: `
    query BlogPosts($first: IntType!) {
      allBlogPosts(first: $first) {
        title
        nonExistingField
      }
    }
  `,
  variables: { first: 10 },
  token: "YOUR_TOKEN",
  includeDrafts: true,
  onUpdate: (update) => {
    // response is the GraphQL response
    console.log(update.response.data);
  },
  onStatusChange: (status) => {
    // status can be "connected", "connecting" or "closed"
    console.log(status);
  },
  onChannelError: (error) => {
    // error will be something like:
    // {
    //   code: "INVALID_QUERY",
    //   message: "The query returned an erroneous response. Please consult the response details to understand the cause.",
    //   response: {
    //     errors: [
    //       {
    //         fields: ["query", "allBlogPosts", "nonExistingField"],
    //         locations: [{ column: 67, line: 1 }],
    //         message: "Field 'nonExistingField' doesn't exist on type 'BlogPostRecord'",
    //       },
    //     ],
    //   },
    // }
    console.error(error);
  },
  onError: (error) => {
    // error is a MessageEvent, the actual error is in error.data
    console.log(error.data);
  },
  onEvent: (event) => {
    // event will be
    // {
    //   status: "connecting|connected|closed",
    //   channelUrl: "...",
    //   message: "MESSAGE",
    //   response: Response
    // }
  },
});
```

## Initialization options

| prop               | type                                                                                       | required           | description                                                                                      | default                              |
| ------------------ | ------------------------------------------------------------------------------------------ | ------------------ | ------------------------------------------------------------------------------------------------ | ------------------------------------ |
| query              | string \| [`TypedDocumentNode`](https://github.com/dotansimha/graphql-typed-document-node) | :white_check_mark: | The GraphQL query to subscribe                                                                   |                                      |
| token              | string                                                                                     | :white_check_mark: | DatoCMS API token to use                                                                         |                                      |
| onUpdate           | function                                                                                   | :white_check_mark: | Callback function to receive query update events                                                 |                                      |
| onChannelError     | function                                                                                   | :x:                | Callback function to receive channelError events                                                 |                                      |
| onStatusChange     | function                                                                                   | :x:                | Callback function to receive status change events                                                |                                      |
| onError            | function                                                                                   | :x:                | Callback function to receive error events                                                        |                                      |
| onEvent            | function                                                                                   | :x:                | Callback function to receive other events                                                        |                                      |
| variables          | Object                                                                                     | :x:                | GraphQL variables for the query                                                                  |                                      |
| includeDrafts      | boolean                                                                                    | :x:                | If true, draft records will be returned                                                          |                                      |
| excludeInvalid     | boolean                                                                                    | :x:                | If true, invalid records will be filtered out                                                    |                                      |
| environment        | string                                                                                     | :x:                | The name of the DatoCMS environment where to perform the query (defaults to primary environment) |                                      |
| contentLink        | `'vercel-1'` or `undefined`                                                                | :x:                | If true, embed metadata that enable Content Link                                                 |                                      |
| baseEditingUrl     | string                                                                                     | :x:                | The base URL of the DatoCMS project                                                              |                                      |
| cacheTags          | boolean                                                                                    | :x:                | If true, receive the Cache Tags associated with the query                                        |                                      |
| reconnectionPeriod | number                                                                                     | :x:                | In case of network errors, the period (in ms) to wait to reconnect                               | 1000                                 |
| fetcher            | a [fetch-like function](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)        | :x:                | The fetch function to use to perform the registration query                                      | window.fetch                         |
| eventSourceClass   | an [EventSource-like](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) class  | :x:                | The EventSource class to use to open up the SSE connection                                       | window.EventSource                   |
| baseUrl            | string                                                                                     | :x:                | The base URL to use to perform the query                                                         | `https://graphql-listen.datocms.com` |

## Events

### `onUpdate(update: UpdateData<QueryResult>)`

This function will be called every time the channel sends an updated query result. The `updateData` argument has the following properties:

| prop     | type   | description                  |
| -------- | ------ | ---------------------------- |
| response | Object | The GraphQL updated response |

### `onStatusChange(status: ConnectionStatus)`

The `status` argument represents the state of the server-sent events connection. It can be one of the following:

- `connecting`: the subscription channel is trying to connect
- `connected`: the channel is open, we're receiving live updates
- `closed`: the channel has been permanently closed due to a fatal error (i.e. an invalid query)

### `onChannelError(errorData: ChannelErrorData)`

The `errorData` argument has the following properties:

| prop     | type    | description                                                        |
| -------- | ------- | ------------------------------------------------------------------ |
| code     | string  | The code of the error (i.e. `INVALID_QUERY`)                        |
| message  | string  | A human-friendly message explaining the error                     |
| fatal    | boolean | If true, the channel has been closed and will not reconnect        |
| response | Object  | The raw response returned by the endpoint, if available (optional) |

### `onError(error: MessageEvent)`

This function is called when connection errors occur (network errors, SSE errors).

The `error` argument is a standard [MessageEvent](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent). The actual error object is available in `error.data`.

### `onEvent(event: EventData)`

This function is called when other events occur.

The `event` argument has the following properties:

| prop       | type     | description                                    |
| ---------- | -------- | ---------------------------------------------- |
| status     | string   | The current connection status (see above)      |
| channelUrl | string   | The current channel URL                        |
| message    | string   | A human-friendly message explaining the event  |
| response   | Response | The HTTP response from the registration request |

## Return value

The function returns a `Promise<() => void>`. You can call the function to gracefully close the SSE channel.

## Releasing (maintainers)

Every user-visible change needs a changeset: run `npx changeset` from the repo
root in the same PR, pick the bump level (`patch` is for bug fixes only, new API
surface is `minor`) and commit the file it writes under `.changeset/`. That is
where the changelog entry comes from, and it is where the bump level is decided
— not on release day. See [`.changeset/README.md`](.changeset/README.md).

To release, from an up-to-date, clean `main`, run `npm run release`. It
builds and tests, applies the pending changesets — bumping the version and
writing `CHANGELOG.md` — publishes to npm, and only then tags `vX.Y.Z`, pushes,
and creates a GitHub release whose notes come straight from that changelog
entry. An interrupted release is resumed by re-running it, never undone. Use
`npm run release:next` for a prerelease under the `next` dist-tag.

The script is
[`@datocms/release-toolchain`](https://github.com/datocms/release-toolchain),
shared with every other DatoCMS repository and pinned here by tag.

# DatoCMS Glossary

Source [glossary]: https://www.datocms.com/glossary.md

A comprehensive guide to terms and terminology related to DatoCMS and Headless CMS that you may encounter when using our product, reading our docs, or getting started with headless content management

## A

### [AEO (Answer Engine Optimization)](https://www.datocms.com/blog/headless-cms-for-llms.md)

AEO is GEO's close cousin, focused on getting your content picked as the direct answer to a question, whether that lands as a featured snippet, a voice assistant reply, or an AI chatbot response. It rewards content that's concise, well structured, and written the way people actually ask things.

### [AI Crawlers](https://www.datocms.com/blog/headless-cms-for-llms.md)

AI crawlers are bots like GPTBot, ClaudeBot, and PerplexityBot that fetch web content to train models or to ground real-time AI answers. Same as search-engine crawlers, you steer them with robots.txt and Content Signals, deciding what they can read, use for answers, or train on.

### AMP

AMP stands for Accelerated Mobile Pages, a technology that creates stripped-down versions of web pages to load blazingly fast on mobile devices. It's all about speed and optimizing for mobile browsing, ensuring users get information swiftly and smoothly.

### [API](https://www.datocms.com/academy/modern-web-development/content-management-apis.md)

API, short for Application Programming Interface, allows different software applications to communicate and interact with each other. It's a set of rules and protocols that enables one piece of software to access and use the services or data provided by another, making it possible for apps, websites, or systems to work together seamlessly. It's the behind-the-scenes magic that helps diverse technologies play nice and collaborate effectively.

### [API-first](https://www.datocms.com/academy/modern-web-development/content-management-apis.md)

API-first means you design your content and services to be consumed through APIs from day one, rather than bolting an API onto an existing monolith. It's a core MACH principle, and the payoff is simple: when everything speaks API, your stack is far easier to connect, extend, and future-proof.

### [API Tokens](https://www.datocms.com/docs/content-management-api/resources/access-token.md)

API tokens are like VIP passes granting users or services access to interact with an API. They're unique keys or strings of characters that serve as authentication—think of them as digital IDs. It's a secure way to verify their identity and permissions, allowing them to make requests and receive responses from the API without compromising sensitive information that they shouldn't have access to.

### [Asset](https://www.datocms.com/docs/general-concepts/media-area.md)

An asset is a file within DatoCMS - this can include images, audio files, videos, and other documents. Dato's Media Area is where you interact with your assets to add, organize, localize, and manage them. DatoCMS also offers an inbuilt image editor and asset metadata manager to make it easier for content editors to work with assets.

### [Audit Logs](https://www.datocms.com/docs/general-concepts/audit-logs.md)

Audit logs keep track of every action and event happening within DatoCMS. They capture a chronological record of all actions within a project, helping to track changes, identify issues, and ensure security by providing a trail of actions taken by users or processes.

## B

### [Blocks](https://www.datocms.com/docs/content-modelling/modular-content.md)

Blocks are unique to DatoCMS, allowing to define complex and repeatable structures that can be embedded inside records. Blocks can be embedded into Structured Text and Modular Content fields to create highly customized and dynamic layouts.

### [Build Triggers](https://www.datocms.com/docs/general-concepts/deployment.md)

Build triggers are action starters in the world of continuous integration and deployment. They're events or conditions that kick off the process of building, testing, and deploying software or websites. DatoCMS natively offers integrations to set up deploy triggers to Netlify, Vercel and GitlabCI.

## C

### [CDA Playground](https://www.datocms.com/docs/content-delivery-api.md)

The Content Delivery API playground is where you can test your queries against DatoCMS's GraphQPI API before moving them into your repo. With inbuilt docs and a GraphQL explorer for all your content records, the CDA playground ensures that your queries are interacting with the right content in a sandbox environment before you query your project on production.

### [CDN](https://www.datocms.com/features/worldwide-cdn.md)

A CDN, or Content Delivery Network is a global network of serversto store and deliver web content like images, videos, and web pages from multiple locations worldwide. When you access a website using a CDN, it serves you data from the server closest to your location. This speed boost reduces load times and ensures a smoother browsing experience for users across the globe.

### [CMS](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md)

A CMS, or Content Management System, is the control center for your content for websites, eCommerce stores, knowledge bases, and other digital platforms. It's a software that allows creating, editing, organizing, and publishing digital content.

### [Collaboration](https://www.datocms.com/docs/general-concepts/collaboration-features.md)

DatoCMS's collaboration tools let manage teamwork and ensure that no data is lost when switching between users. With features like Content Stages, Presence Indicators, Record Locking, Workflows, and more, DatoCMS encourages teams to work together without losing time and effort on manual back-and-forth on creating content.

### [Composable](https://www.datocms.com/blog/what-is-composable-architecture-and-how-to-implement-it.md)

"Composable" software is a growing approach to companies building their ideal tech stacks using modular APIs and microservies rather than buying all-in-one suites. Related to the MACH (Microservices, API-first, Cloud-native, and Headless) approach, going composable allows companies to work with software that's flexible, scalable, and efficient to their particular business needs.

### [Content as a Service (CaaS)](https://www.datocms.com/blog/what-is-content-as-a-service.md)

Content as a Service delivers your content through APIs so it can be reused across any channel, from your website to an app to a kiosk to a smartwatch, all from one source. Instead of trapping content inside one platform's templates, CaaS treats it as structured data you can send anywhere.

### [Content Delivery API](https://www.datocms.com/docs/content-delivery-api.md)

The Content Delivery API, or CDA, is used to retrieve content from your DatoCMS projects and deliver it to your web or mobile projects. Our CDA is written in GraphQL, and all content is served via a robust CDN with multiple datacenters around the world to ensure minimal latency.

### [Content Governance](https://www.datocms.com/features/workflow-cms.md)

Content governance is the set of rules, roles, and workflows that keep content accurate, consistent, and on-brand as teams and volume grow. It covers who can create and publish what, how content gets reviewed, and how standards are enforced: the guardrails that stop a big content operation descending into chaos.

### [Content Link (prev: Visual Editing)](https://www.datocms.com/docs/content-link/how-to-use-content-link.md)

Content Link is a feature supported on deployments to Vercel, using Vercel's site previews. Content on your frontend previews corresponding to text, structured text, or the \`alt\` field of any asset fields within a DatoCMS project get a \`Open in DatoCMS\` link, allowing content editors to quickly made changes to content without needing to know where to navigate to.

### [Content Management API](https://www.datocms.com/docs/content-management-api.md)

The Content Management API, or CMA, is used to manage the content of your DatoCMS projects. This includes creating, updating, deleting, and fetching content of your projects, featuring 40+ resources and 150+ endpoints.

### [Content Modeling](https://www.datocms.com/academy/headless-cms/how-a-headless-cms-works.md)

Content modeling is the process of designing the skeleton for your website's content. It involves defining the various content types, their attributes, relationships, and how they'll be organized.

### Content Signals

Content Signals are directives you add to robots.txt to tell AI crawlers how your content can be used: for search, as input to AI answers, or for training models. They give you a machine-readable way to set your terms, so you can welcome the citations while drawing a hard line exactly where you want one.

### [Content Type](https://www.datocms.com/docs/content-modelling.md)

A content type (sometimes called a model) is the blueprint for a particular kind of content, like a blog post, a product, or an author, including all its fields and rules. Define it once and every record you create follows the same predictable shape.

### [Content Versioning](https://www.datocms.com/docs/general-concepts/versioning.md)

Content versioning keeps a full history of changes to your records, so you can see what changed, who changed it, and roll back whenever you need to. It's a safety net for content teams: experiment freely, because a previous version is always one click away.

### [Content Workflow](https://www.datocms.com/features/workflow-cms.md)

A content workflow is the defined path content travels from draft to published, through stages like review and approval, usually with roles controlling who can do what. It keeps bigger teams organized and makes sure nothing goes live before it's actually ready.

### [Core Web Vitals](https://www.datocms.com/academy/headless-cms/headless-cms-and-seo.md)

Core Web Vitals are Google's set of user-experience metrics (loading, interactivity, and visual stability) that measure how a page actually feels to real visitors. They're also a ranking signal, which makes performance a direct SEO concern rather than a nice-to-have.

## D

### [DAM (Digital Asset Management)](https://www.datocms.com/docs/general-concepts/media-area.md)

A DAM system stores, organizes, and delivers your digital assets like images, video, and documents, with metadata, search, and access controls. In a modern stack it keeps your media tidy and reusable across every channel, usually with on-the-fly transformations baked in.

### [Data Residency](https://www.datocms.com/security.md)

Data residency is about where your content is physically stored and processed, meaning which country or region the servers actually live in. It matters for performance, but mostly for compliance, since rules like GDPR can require certain data to stay inside specific borders.

### [Decoupled CMS](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md)

A decoupled CMS splits content management from presentation, but unlike a purely headless CMS it often still ships a built-in front end you can use if you want. You get the flexibility of an API-driven backend with a ready-made delivery layer within arm's reach.

### [DXP](https://www.datocms.com/blog/headless-cms-vs-dxp-an-in-depth-comparison.md)

A DXP, or a Digital Experience Platform, is a category of software aiming to tackle the primary goal of perfecting the customer experience, or CX. DXPs can be a full suite, or a composable stack of complementary APIs brought together to create a modular DXP.

## E

### [Edge Rendering](https://www.datocms.com/academy/modern-web-development/deployments.md)

Edge rendering runs your page logic on servers physically close to your visitors, out at the edge of the network instead of one central box. You get dynamic, personalized content delivered at nearly the speed of a static file, because the work happens milliseconds away from the user.

### Embeddings

Embeddings are numerical representations of text or images that capture their meaning, so machines can measure how similar two pieces of content are. They're the engine behind semantic search and RAG: turn your content into vectors and an AI can find the most relevant record even when the exact words don't match.

### [Environments](https://www.datocms.com/docs/content-management-api/setting-the-environment.md)

Environments are like different playgrounds for testing and deploying changes to the schema within the CMS. The primary environment is for editors to author records. Ideally, developers should not make changes to its schema directly, but fork it, test the changes on sandbox environments, and then merge the changes via migration scripts.

## F

### [Field Type](https://www.datocms.com/docs/content-modelling.md)

Each content model consists of multiple field types - within DatoCMS the fields available are Single-line strings, Multiple-paragraph text, Modular content, Structured text, Asset galleries, Single assets, Videos, Date and DateTime, Integers, Booleans, Geolocations, Colors, SEO meta tags, Slugs, Links (relations), and JSON.

### [Field Validation](https://www.datocms.com/docs/content-modelling/validations.md)

Validations are a strong tool to enforce content structure and integrity by enforcing certain restrictions on what content can be created. This can be something as simple as ensuing entries are localized, to something more complex like having a rule for content entries to match a specific regex pattern. Each field type in DatoCMS offers varied applicable validations.

### [Focal Point](https://www.datocms.com/features/images-api.md)

A focal point marks the most important part of an image, so automatic crops for different aspect ratios keep the subject in frame. Set it once in your CMS and every thumbnail and hero crop stays sensible, with no more accidentally decapitated headshots.

## G

### [GEO (Generative Engine Optimization)](https://www.datocms.com/blog/headless-cms-for-llms.md)

GEO is how you get your content surfaced and quoted by AI answer engines like ChatGPT, Perplexity, and Google's AI Overviews. Classic SEO chases blue links. GEO is about being the source the model actually cites, which comes down to clean, structured, machine-readable content an LLM can parse and trust.

### [GraphQL](https://www.datocms.com/academy/modern-web-development/graphql-vs-rest.md)

GraphQL is a modern query language and a runtime for APIs, widely seen as a successor to REST APIs. It's built around the concept of "getting exactly what you asked for", without any under fetching or over fetching of data. GraphQL also makes it easier to aggregate data from multiple sources, allowing you to source from multiple locations within a single query, rather than fiddling with several endpoints.

## H

### [Headless CMS](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md)

A headless CMS is a backend-only CMS that provides an API to make content accessible to any platform or digital channel. Unlike a traditional CMS such as WordPress or Drupal, a headless CMS does not dictate where or how content is shown - but rather requires teams to build their own custom frontend (or presentation layers) using their preferred frameworks like React, Angular, and Vue.

### History resolution

Period in which bursts of changes made to the same record by the same user (or API token) will be grouped into a single revision version. Versions created in the last 7 days do not get grouped.

### [History Retention](https://www.datocms.com/docs/general-concepts/versioning.md)

DatoCMS retains all content history changes for a specific amount of time, to refer back to as required when making changes or reverting content to a previous state. The content retention period is defined by which plan you're on.

### [Hybrid CMS](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md)

A hybrid CMS mixes the API-first flexibility of headless with the convenience of built-in presentation tools, so teams can pick per project. Developers get clean APIs for custom frontends, marketers keep their familiar previews and page building, and nobody has to pick a side.

### [Hydration](https://www.datocms.com/academy/ancillary-concepts/astro-concepts.md#partial-hydration)

Hydration is the step where JavaScript attaches behavior to server-rendered HTML once it reaches the browser, turning a static page into an interactive one. Modern tricks like partial or lazy hydration do this selectively to keep things fast, rather than booting up everything at once.

## I

### [Image Optimization](https://www.datocms.com/features/images-api.md)

Image optimization is the art of shipping the smallest possible image that still looks great, through compression, modern formats, and correct sizing. Since images are usually the heaviest thing on a page, getting this right is often the fastest route to a faster site.

### [Images API](https://www.datocms.com/features/images-api.md)

The Images API within DatoCMS is built upon Imgix, providing a best-in-class API for image processing along with a global image CDN. The Images API allows for powerful features out of the box, such as asset localization, image editing, watermarketing, and image optimization.

### [Internationalization (i18n)](https://www.datocms.com/features/headless-cms-multi-language.md)

Internationalization is designing your content and systems up front so they can support multiple languages and regions without a rebuild later. Nail i18n and localization becomes the easy part, because the structure is already there and you're just filling it in.

### [Islands Architecture](https://www.datocms.com/academy/ancillary-concepts/astro-concepts.md#wtf-are-astro-islands)

Islands architecture ships a mostly static HTML page and only hydrates the interactive bits, the islands, with JavaScript. Instead of loading a heavy framework for the whole page, you keep it light and only pay for interactivity where you actually need it.

### [ISR (Incremental Static Regeneration)](https://www.datocms.com/academy/modern-web-development/modern-web-development-concepts.md)

ISR is the happy middle ground between static and server rendering. Pages are served statically for speed, but quietly regenerated in the background on a schedule or on demand, so you get CDN-fast delivery without rebuilding the whole site every time one record changes.

## J

### [Jamstack](https://www.datocms.com/academy/modern-web-development/frameworks-and-technologies.md)

Coined by Netlify, Jamstack is a modern approach for building websites or web apps. It stands for JavaScript, APIs, and Markup. Instead of relying on traditional server-side rendering, it pre-builds the website and serves it through a CDN as flattened HTML. This approach boosts speed, security, and scalability by separating the front-end presentation from the back-end.

## L

### [Lazy Loading](https://www.datocms.com/features/images-api.md)

Lazy loading holds off on loading images and other heavy assets until they're about to scroll into view, so the initial page loads faster and lighter. Visitors only download what they actually see, which is kinder to both load times and data plans.

### [Links](https://www.datocms.com/docs/content-modelling.md)

Links, also referred to as Relations or References, are bi-directional links between multiple content records and types. A common use case could be seeing which blog posts are related to a single author, or which landing pages match a certain category.

### [llms.txt](https://www.datocms.com/blog/llms-txt.md)

llms.txt is a simple Markdown file you drop at the root of your site to hand AI models a clean, curated map of your best content. Think robots.txt, but for large language models: instead of making crawlers dig through your nav and markup, you give them a tidy index of what matters and where to find it.

### [Locale Fallback](https://www.datocms.com/features/headless-cms-multi-language.md)

Locale fallback is what kicks in when content isn't available in a visitor's language: the system serves a default locale instead of showing them nothing. It means every reader gets a complete page, even while some translations are still catching up.

### [Localization (l10n)](https://www.datocms.com/features/headless-cms-multi-language.md)

Localization is the work of adapting your content for a specific locale, translating copy but also adjusting dates, currencies, imagery, and tone so it feels native to that audience. It's the "making it local" half of going global.

## M

### [MACH](https://www.datocms.com/blog/why-you-need-a-headless-cms-for-mach-architecture.md)

MACH stands for Microservices, API-first, Cloud-native, and Headless—a set of principles shaping modern digital architectures. As a modern approach to composable enterprise software strategy based on smaller solutions that seamlessly integrate with one another, it is a base for a modern enterprise approach to keep their stack pluggable, scalable, and replaceable.

### [Maintenance Mode](https://www.datocms.com/docs/content-management-api/resources/maintenance-mode.md)

Activating maintenance mode in DatoCMS means making the primary environment read-only. This is particualrly useful when larger changes need to be made to the primary environment (schema migrations from sandbox environments, etc.), in order to ensure no content changes or updates are done to the CMS project until the operation is complete.

### [MCP (Model Context Protocol)](https://www.datocms.com/docs/mcp-server.md)

MCP is an open standard that lets AI assistants plug into external tools and data through one consistent interface. Instead of every app inventing its own integration, an MCP server exposes its capabilities so agents like Claude or ChatGPT can read, query, and act on your content directly. We run our own remote MCP server, so you can manage DatoCMS straight from your favorite AI client.

### [Media Area](https://www.datocms.com/docs/general-concepts/media-area.md)

The Media Area is where you interact with all your files and assets in DatoCMS. DatoCMS accepts all formats for images, videos, audio, and documents, making it simpler to manage all your content from a single place. Aside from Digital Asset Management (DAM), DatoCMS also allows plugins into the media area such as Unsplash for stock images, or Dall-E for AI image generation.

### [Migrations](https://www.datocms.com/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md)

DatoCMS allows you to programmatically add models and records using migrations, either via writing scripts manually, or by having the DatoCMS CLI automatically generate migrations for you.

### [Modern Image Formats (WebP / AVIF)](https://www.datocms.com/features/images-api.md)

WebP and AVIF are next-gen image formats that hit the same visual quality as JPEG or PNG at a fraction of the file size. Serving them automatically, with older formats as a fallback, is one of the easiest wins going for faster pages and happier Core Web Vitals.

### [Modular Content](https://www.datocms.com/docs/content-modelling/modular-content.md)

Modular content fields are used to define dynamic areas for customized page layouts, rather than restricting content editors to use rigid templates. For example, when building a blog post, the content editor can add blocks such as CTAs, quotes, or videos into the article depending on which blocks are accepted into the article field.

## O

### [Omnichannel](https://www.datocms.com/blog/omnichannel-cms.md)

Omnichannel means delivering consistent content and experiences across every channel a customer touches: web, mobile, email, in-store, voice, the lot. A headless, API-driven CMS is what makes it doable, because you model your content once and publish it everywhere without copy-pasting.

### [Organization](https://www.datocms.com/docs/general-concepts/organizations-and-accounts.md)

Projects on DatoCMS can be tied to an organization. Particularly useful for teams or agencies handling multiple projects or clients, an organization allows for better visibility and shared ownership between DatoCMS projects. Similar to personal accounts, organizations can also upgrade to paid plans and manage billing information.

## P

### [Per-locale Publishing](https://www.datocms.com/features/headless-cms-multi-language.md)

Per-locale publishing involves releasing or making content available based on specific regions or languages. Content is published or displayed selectively, targeting particular locales or language groups. This approach ensures that the right content reaches the right users in their preferred language or region, without making unnecessary changes to all content unintentionally.

### [Plugins](https://www.datocms.com/marketplace/plugins.md)

Plugins allow you to easily expand and customize the capabilities of DatoCMS. With popular plugins for services like SEO readability, cloud deployments, computed fields, Shopify products, and more, plugins are React applications communicating with your DatoCMS project to enable custom functionalities and workflows, as well as to interact with external APIs without leaving your project.

### [Project Starter](https://www.datocms.com/marketplace/starters.md)

A project starter is an out-of-the-box DatoCMS project built with a complete CMS configuration, corresponding frontend, and integrations to popular deployment platforms like Netlify and Vercel. They help users rapidly set up common projects (like blogs or websites) and get familiar with best practices when working with a Headless CMS.

## R

### [RAG (Retrieval-Augmented Generation)](https://www.datocms.com/blog/headless-cms-for-llms.md)

RAG makes AI answers more accurate by pulling relevant, up-to-date content at query time and feeding it to the model as context, instead of relying only on what the model memorized during training. For content teams that's the whole pitch: a well structured CMS becomes the trusted knowledge base an AI grounds its answers in.

### [Real-time Updates API](https://www.datocms.com/docs/real-time-updates-api.md)

The Real-time Updates API (sometimes refered to as a Subscriptons API) allows client to listen for content changes via a stable connection to stream events as they occur. This is particularly useful for real-time content refreshing for use-cases like live sports updates or stock prices, when new content needs to appear without a user having to refresh the page.

### [Record](https://www.datocms.com/docs/content-management-api/resources/item.md)

A record is any content entry you make within your DatoCMS project based on your content models and schema - think of it as a table row in a database. This can include anything from a blog post, to a landing page or a hosted video.

### [Record Info](https://www.datocms.com/docs/content-management-api/resources/item.md)

Record information are system records for each content entry within a DatoCMS project that can be queried via the CDA - this includes metadata such as the Record ID, publishing status, timestamps for creation, and information on publishing.

### [Reference Field](https://www.datocms.com/docs/content-modelling.md)

A reference field links one record to another, connecting a blog post to its author or a product to its category, so you model real relationships instead of duplicating data. It's how structured content stays connected and DRY: define a record once and reuse it wherever you need it.

### [Responsive Images](https://www.datocms.com/features/images-api.md)

Responsive images serve the right image size and format for each device and screen, so phones aren't downloading desktop-sized files. Delivered through an images API, they're one of the highest-impact things you can do for page speed and Core Web Vitals.

### [Rich Text](https://www.datocms.com/docs/content-modelling/structured-text.md)

Rich text is formatted content like headings, bold, links, and embedded media, created in a friendly editor without touching code. The good implementations store it as clean, structured data rather than raw HTML, so the same content renders beautifully on any platform.

### [Roles and Permissions](https://www.datocms.com/docs/content-management-api/resources/role.md)

Each user and API Token can be assigned specific roles within DatoCMS, with a custom and pre-defined set of permissions they're allowed to perform. These can range anywhere from being able to create content in a specific locale, moving content between a specific stage, or reading content from a specific environment.

## S

### [Scheduled Publishing](https://www.datocms.com/docs/general-concepts/scheduled-publishing-unpublishing.md)

Scheduled publishing is a feature in DatoCMS that allows you to plan when content goes live (or unpublished) on your website or platform. Instead of immediately publishing, you can schedule articles, updates, or posts to appear at a specified date and time. It's a handy tool for content creators and marketers, ensuring timely and organized releases without manual intervention.

### [Schema](https://www.datocms.com/docs/general-concepts/data-modelling.md)

Schema refers to the structure that defines the organization and format of content within DatoCMS or any other CMS. It's like the framework that outlines how data should be arranged (content records under content models), including the types of data (fields), their relationships, and constraints.

### Semantic Search

Semantic search actually understands the intent behind a query instead of just matching keywords. Powered by embeddings and vector search, it returns results that mean the same thing as what you asked, so it's far more forgiving of natural, conversational phrasing than old-school keyword search.

### [Single-instance Models](https://www.datocms.com/docs/content-modelling/single-instance.md)

Many website use-cases call for single-use templates or content types, such as homepages, about-us pages, or navigations. Single-instance models are useful for content that isn't meant to be duplicated or exist across multiple places. When creating a new model, simply select \`Single Instance\` within DatoCMS to enable this for content editors.

### Single Source of Truth

A single source of truth is one central, authoritative place where each piece of content lives, so every channel pulls from the same well-governed data. It kills the drift and duplication you get when the same info is maintained across five different systems.

### [Site Search](https://www.datocms.com/docs/site-search.md)

DatoCMS offers an in-built approach to implementing Site Search results to your website visitors. With minimal configuration needed, new content is crawled and fetched with every new deploy. This can be an ideal approach for most websites, however for advanced search use-cases, DatoCMS offers plugins and integrations to popular services like Algolia and Cludo.

### Slug

A slug is the human-readable, URL-friendly ID for a piece of content, like "my-first-post" in yoursite.com/blog/my-first-post. Keep them short, descriptive, and stable, because good slugs are better for readers, better for SEO, and better for anyone trying to make sense of your links.

### [SSG (Static Site Generation)](https://www.datocms.com/academy/modern-web-development/modern-web-development-concepts.md)

Static Site Generation pre-builds your pages into plain HTML at build time, so they can be served instantly from a CDN with no per-request server work. Fast, secure, cheap to scale. The catch is that content changes need a rebuild, which is exactly where a headless CMS and build triggers earn their keep.

### [SSO](https://www.datocms.com/security.md)

Single Sign-On, or SSO, is a user authentication process that allows individuals to access multiple applications or systems with just one set of login credentials, using common SSO services like Okta and OneLogin. SSO is currently an enterprise feature on DatoCMS.

### [SSR (Server-Side Rendering)](https://www.datocms.com/academy/modern-web-development/modern-web-development-concepts.md)

Server-Side Rendering builds each page on the server the moment it's requested, so visitors always get fresh, personalized content. Great for pages that change constantly or depend on the user, at the cost of a bit more work than serving a pre-built static file.

### [Structured Text](https://www.datocms.com/docs/content-modelling/structured-text.md)

Structured text in DatoCMS is a field type that allows content editors to create rich content entries with a powerful editor allowing for \`/\` slash commands to embed other blocks like images, galleries, and embeds. The content is stored in a semantic JSON format, making it simple to query.

## T

### [Taxonomy](https://www.datocms.com/docs/content-modelling/trees.md)

Taxonomy is how you classify and organize content with categories, tags, and hierarchies, so it's easy to browse, filter, and relate. A good taxonomy is what turns a pile of records into a navigable, connected content library.

### [Tree-like Models](https://www.datocms.com/docs/content-modelling/trees.md)

Tree-like models enable for common use cases where content needs to exist in a hierarchial structure. Common use-cases for this include examples like eCommerce categories, taxonomies, and product navigation, allowing content creators to manage content in a tree-like data structure. DatoCMS lets you create these by enabling \`Records can be organized in a tree\` for content editors.

## V

### Vector Search

Vector search finds content by meaning instead of exact keywords, comparing the embeddings of your query against the embeddings of your content. Ask for "ways to speed up my site" and it can surface an article called "Improving performance" with zero shared words. It's what powers modern AI search and recommendations.

### [Video API](https://www.datocms.com/features/video-api.md)

DatoCMS's Video API, powered by Mux, provides powerful features such as adaptive bitrate, global caching, and thumbnails, ensuring videos are loaded and streamed exceptionally quickly with the right format for each device, on any player.

## W

### [Webhooks](https://www.datocms.com/docs/general-concepts/webhooks.md)

Webhooks deliver real-time notifications or data to other applications or systems whenever a predefined event occurs. Instead of constantly asking for updates, webhooks wait for triggers, such as a new order or a form submission, and then immediately send that data to a predefined URL or endpoint. They're a way for different systems to communicate efficiently, facilitating timely actions based on events.

---

# Become a Partner

Source [marketing]: https://www.datocms.com/partner-program.md

## DatoCMS Agency Partner Program


Agency-specific plans **from €39/month**. No sales targets. No reselling requirements. No BS. Just real benefits including 30% off for your clients and free access to every project you manage.

### Partnerships done right

DatoCMS was born out of an agency. It's only natural that our partner program incorporates all the essential elements to support the success of your agency.

#### 💰 Special plans and deals

Custom plans to get you started without surprises, and a 30% discount on the Professional plan for your clients.

#### 🎁 Discounts for your clients

Enable special plans on your clients' accounts [directly from your dashboard](https://www.datocms.com/docs/agency-partner-program/partners-dashboard.md#enabling-special-plans-to-clients) , no need to ask us.

#### 🔑 Full access to your clients' projects

Give your team [predefined access to all client projects](https://www.datocms.com/docs/agency-partner-program/partners-dashboard.md#automatic-access-to-your-clients-projects) without any extra collaborator seats.

#### 🧙‍♂️ Dedicated partner manager

Direct access to our Partner Team for you and your clients, with constant updates and support.

#### 🔖 Dedicated partner listing

Get listed on our [Partners page](https://www.datocms.com/partners.md) with your projects and co-marketing content so teams looking for development help can find you.

[Explore our partner listings →](https://www.datocms.com/partners.md)

#### 🫶 Co-marketing

We'll promote your work through [case studies](https://www.datocms.com/customers.md), articles, and other collaborations. We also LOVE to talk to you about your work.

[Explore our partner chats →](https://www.datocms.com/casual-chats.md)

## Agency partners unlock plans starting at just €39/month

Yeah, that's not a typo. We designed our partnership program to help you find new clients, gain more flexibility, and receive assistance in implementing projects with DatoCMS.

### Getting started is easy

Join our Partner Program and start your journey to become a DatoCMS Partner today.

1.  01
    
    Submit the form
    
    Fill in the form below to share some details, and tell us a bit about who you are and what you're working on.
    
2.  02
    
    Complete the enrollment
    
    While you can already enjoy the benefits, activate a paid plan and fill out your public Partner profile for us to get you listed.
    
3.  03
    
    It's official
    
    Utilize the support of our partnership and grow your business. We'll reach out about co-marketing opportunities all the time!

---

# When to choose DatoCMS over Sanity?

Source [compare]: https://www.datocms.com/compare/datocms-vs-sanity.md

Our customers prefer DatoCMS for its convenient scalability, unrivaled developer experience, and clean editing interface.

## DatoCMS vs. Sanity: How we're different

DatoCMS is the go-to for tech teams that want a **speedy switch to headless**. Its plug-and-play nature gives developers exactly what they need, **with zero overkill**.

Sanity is a great option for developers that need **full control over every aspect** of their headless CMS, even if it means **slowing down setup and deploy**.

## At a glance comparison

### Who is it for

**DatoCMS:** Designed for tech teams that want to move to headless with **a CMS that gets out of the way**, so they can get straight to the good part - building.

**Sanity:** Built for tech teams that don't mind trading some velocity for the luxury of getting **full control** over every aspect of their CMS.

### Setup and maintenance

**DatoCMS:** Built for **rapid deployments**, so your team can hit the ground running with an optimal setup ASAP.

**Sanity:** Every aspect of the CMS can be tweaked, but needs a certain **ramp-up time to get everything setup**.

**DatoCMS:** Updates and improvements **happen automatically**, unless they might cause breaking changes - in that case, it's your call.

**Sanity:** Needs **manual work** to upgrade to new versions.

### Developer experience

**DatoCMS:** Lay down the **perfect groundwork** with ease, thanks to the native [GraphQL API](https://www.datocms.com/features/headless-cms-graphql.md).

**Sanity:** Its native query language, GROQ, is **powerful but proprietary** and you'll need to learn it. GraphQL is offered only as an alternate option.

**DatoCMS:** [**Native video streaming**](https://www.datocms.com/features/video-api.md) that's easy on the wallet, with adaptive outputs for any device.

**Sanity:** You'll have to **build video streaming** from scratch, or wrangle by integrating a 3rd-party provider.

### Content Modeling

**DatoCMS:** [Visual content modeling](https://www.datocms.com/features/dynamic-layouts.md) lets you **quickly assemble and fine-tune** **models** without unnecessary complexity.

**Sanity:** You'll have to **code your schemas** from scratch. They're totally customizable, but be ready for the extra developer work that comes with that flexibility.

**DatoCMS:** **40+ built-in** [**field validations**](https://www.datocms.com/features/data-integrity.md) to keep your data in check and make editing a breeze, no setup neeed.

**Sanity:** Expect to **put together your own validations** using Sanity's building blocks. These checks are only client-side, which might compromise your data integrity.

**DatoCMS:** **Validates all existing records** with schema changes **during migrations**, ensuring compliance with the new structure and the validation rules.

**Sanity:** **Doesn’t enforce validation rules** for each field **during migrations**, resulting in breaking changes if a record is not compliant.

### Editor Experience

**DatoCMS:** **Clean and approachable editing interface**, where every role can get involved and be productive from the very first day.

**Sanity:** Comes with a **stripped-down editing UI** within Content Studio that won't meet all your needs, unless you're willing to put in significant development effort for customization.

**DatoCMS:** Set up an intuitive [Landing Page Builder](https://www.datocms.com/features/dynamic-layouts.md) with repeatable Blocks, providing editors with a flexible way to **mix and match page elements**.

**Sanity:** Building landing pages is doable with the provided object-types, but the **excessive usage of modals limits authors** from seeing a full-page view at any time.

**DatoCMS:** [**Handle localization with ease**](https://www.datocms.com/features/headless-cms-multi-language.md) thanks to native capabilities like per-field localization settings, locale-based publishing, and translator roles.

**Sanity:** **Lacks built-in localization features** - even basic tasks like adding a new locale or localizing a field could disrupt your project.

### Integrations

**DatoCMS:** Connect to external systems and databases using **native plugins**, or by developing **custom plugins** to suit specific needs.

**Sanity:** Focused on **business-wide content centralization** through their 'Content Lake' which can be used with sources like ERPs, e-commerce platforms, etc.

## Three reasons to consider DatoCMS

1.  Lightning-fast performance
    
    With a global CDN and optimized media delivery, your projects get an impressive boost in speed, plus improved SEO rankings and user engagement. It’s what your projects need to match the swift pace of today’s web.
    
2.  Delightful developer experience
    
    We’ve designed DatoCMS as a flexible platform to build precisely what you need. Our native GraphQL API and visual content modeling give you the freedom to build a content infrastructure that adapts to any scenario.
    
3.  A perfect blend of easy and powerful
    
    Our editing interface feels like a friendly wave from Wordpress, but with more power under the hood. Get creative with features like Blocks and Modular Content for a no-fuss, no-code page building experience that still packs a serious punch.

---

# When to choose DatoCMS over Contentful?

Source [compare]: https://www.datocms.com/compare/datocms-vs-contentful.md

Our customers prefer DatoCMS for its convenient scalability, unrivaled developer experience, and clean editing interface.

## DatoCMS vs. Contentful: How we're different

DatoCMS is the headless option designed for **SMEs, agencies,** and lean teams in **larger companies,** seeking a **cost-effective** alternative to Contentful.

Contentful is focused on **larger organizations** that need extensive professional services, and are **not limited by budget** when considering a switch to headless.

## At a glance comparison

### Who Is It For

**DatoCMS:** Designed to help **SMBs and agencies** and provide the tools and flexibility that small and medium-sized projects need to succeed.

**Contentful:** Designed to support **enterprise companies** with complex needs, including extensive professional services.

### Pricing

**DatoCMS:** **Flexible pricing** that respects your budget, without the pressure to upgrade to an Enterprise-level plan too soon.

**Contentful:** Small and medium-sized applications are quickly **pushed into costly Enterprise tiers** with steep price jumps - expect to engage in frequent sales negotiations.

### Developer Experience

**DatoCMS:** Tap into DatoCMS's full potential with [native GraphQL APIs](https://www.datocms.com/features/headless-cms-graphql.md) that can handle even the **most complex queries** and return only the **essential data** for your front-end

**Contentful:** The API range is somewhat restrictive, often resulting in **unoptimized responses** that are harder to manage on the front-end.

**DatoCMS:** The [Images API](https://www.datocms.com/features/images-api.md) comes out-of-the-box with **automatically optimized outputs** and **150+** powerful transformation options.

**Contentful:** Simple Images API with **10 basic** transformation options and **no automatic optimization**.

**DatoCMS:** [**Native video streaming**](https://www.datocms.com/features/video-api.md) that's easy on the wallet, with adaptive outputs for any device.

**Contentful:** You'll have to **build video streaming** from scratch, or wrangle by integrating a 3rd-party provider.

### Content Modeling

**DatoCMS:** With a [flexible content modeling](https://www.datocms.com/features/dynamic-layouts.md), you can establish models and reuse content with ease, resulting in **clean content collections** and **easier editorial work**.

**Contentful:** **Limited content organization options** lead to simpler, less hierarchical collections and increase the likelihood of errors during the editing process.

**DatoCMS:** [**Handle localization with ease**](https://www.datocms.com/features/headless-cms-multi-language.md) thanks to native capabilities like per-field localization settings, locale-based publishing, and translator roles.

**Contentful:** **Localization is far from perfect** due to an awkward interface that complicates the process, especially when there's multiple locales.

### Governance

**DatoCMS:** One **central hub and app** to handle every aspect of your content.

**Contentful:** Juggle **multiple specialized applications** like Launch, Compose, and the default UI to manage different content areas.

**DatoCMS:** A **rich permissions system**, designed to fit the majority of projects, with the added flexibility of custom roles on all paid plans.

**Contentful:** **High-grade permission and workflow management** that cover any scenario; however, the ability to create custom roles is reserved to custom enterprise plans.

### Extensibility

**DatoCMS:** While the range of technical partnerships and third-party integrations may be select, **anyone can develop and share custom plugins** in the marketplace.

**Contentful:** Wide array of technical partnerships and integrations, along with a framework for creating custom apps. However, **only Contentful can publish apps on the marketplace**.

### Support

**DatoCMS:** Small but **tight team of friendly experts** to help every customer get the most out of the product.

**Contentful:** A clear divide in service levels, as higher-paying customers receive enhanced support and SLAs, whereas **smaller clients end up being neglected**.

## Ready to migrate from Contentful to DatoCMS?

We've got your back - the Contentful importer does all the heavy lifting, so you can transition your Contentful space to a fresh DatoCMS project in minutes.

[Learn More](https://www.datocms.com/docs/import-and-export/import-space-from-contentful.md)

## Three reasons to consider DatoCMS

1.  Lightning-fast performance
    
    With a global CDN and optimized media delivery, your projects get an impressive boost in speed, plus improved SEO rankings and user engagement. It’s what your projects need to match the swift pace of today’s web.
    
2.  Delightful developer experience
    
    We’ve designed DatoCMS as a flexible platform to build precisely what you need. Our native GraphQL API and visual content modeling give you the freedom to build a content infrastructure that adapts to any scenario.
    
3.  A perfect blend of easy and powerful
    
    Our editing interface feels like a friendly wave from Wordpress, but with more power under the hood. Get creative with features like Blocks and Modular Content for a no-fuss, no-code page building experience that still packs a serious punch.

---

# When to choose DatoCMS over Wordpress?

Source [compare]: https://www.datocms.com/compare/datocms-vs-wordpress.md

Our customers prefer DatoCMS for its convenient scalability, unrivaled developer experience, and clean editing interface.

## DatoCMS vs. Wordpress: How we're different

DatoCMS is the right option for agencies and tech teams that want to embrace headless and **speed up development**, while maintaining a **smooth experience for editors**.

Wordpress is the juggernaut CMS on the market, but it's also known for its **heavy-load maintenance** and bloated plugin system, **leading to performance lags**.

## At a glance comparison

### Who is it for

**DatoCMS:** Built for tech teams that want to **speed up development pace** and leave antiquate systems behind, ensuring editors keep a user-friendly platform.

**Wordpress:** The **world’s most adopted CMS**, powering 43% of the internet, well known for being easy to use, but still fairly customizable through plugins.

### Costs

**DatoCMS:** **Transparent pricing** with no hidden costs, and a full-featured free tier to experiment before committing.

**Wordpress:** Starts free, but **costs add up quickly** - premium plugins and themes, hosting, engineering - resulting in dozens or even hundreds of dollars per month for larger-scale projects.

### Developer Experience

**DatoCMS:** Gives engineers the freedom to **choose any tech stack and framework**, like Next.js, Vue, React, as the content is separated from the presentation layer, **without worrying about the backend**.

**Wordpress:** While integrating a chosen frontend using WordPress APIs is doable, it **comes with its challenges** — you'll still need to **handle backend tasks** to ensure scalability and maintaining security.

**DatoCMS:** **Frontend development demands time**, but opens the door to user experiences that are both **custom-fit** and **technologically superior**.

**Wordpress:** An all-encompassing solution allows for **rapid site building** and deployment, but venturing into **customizations quickly becomes complex** and time-consuming.

### Editor experience

**DatoCMS:** A clean editing experience where editors coming from Wordpress can **transition quickly** and be productive from the get-go.

**Wordpress:** The editor delivers an environment that has **achieved widespread adoption** and recognition throughout industries.

### Extensibility

**DatoCMS:** Extend UI and functionality using a mix of official, community, and private plugins. However, **they may lack the capabilities of WordPress's**, and often require frontend modifications for integration.

**Wordpress:** Offers a **huge selection of plugins and themes** to enhance a site's functionality and appearance, optimizing for SEO, security, and user experience without any development.

### Performance & Scalability

**DatoCMS:** Without the burden of managing backend scalability, developers can **channel their creativity** into actually building their projects.

**Wordpress:** Scalability hinges on the backend's web and database servers, **placing a burden on your IT** or forcing reliance on **expensive hosting services**.

**DatoCMS:** Includes **media management with storage and CDN** on Imgix [for images](https://www.datocms.com/features/images-api.md) and Mux [for video optimization](https://www.datocms.com/features/video-api.md).

**Wordpress:** Plugins can enhance multimedia content with a CDN to boost performance, but **they are not cheap** and can lead to **vendor lock-in**.

### Security & Maintenance

**DatoCMS:** **Enterprise-grade network security** with DDOS mitigation, TLS encryption at rest and in transit, network rate limits, and two layers of caching to minimize downtime risks, with an average 99,99% uptime.

**Wordpress:** **Widely known for security issues** due to vulnerabilities in plugins, themes, and outdated versions.

**DatoCMS:** Sit back and relax, no need to worry about backend maintenance - DatoCMS **keeps the solution up-to-date for you.**

**Wordpress:** Navigating updates across themes, plugins, and various software layers can be a minefield that often leads to site disruptions, **turning maintenance into a tricky endeavor**.

## Ready to migrate from Wordpress to DatoCMS?

Switching to DatoCMS is a breeze with our Wordpress importer. It takes care of the complexities, allowing you to move your site to a new DatoCMS project in no time at all.

[Learn More](https://www.datocms.com/docs/import-and-export/import-from-wordpress.md)

## Three reasons to consider DatoCMS

1.  Lightning-fast performance
    
    With a global CDN and optimized media delivery, your projects get an impressive boost in speed, plus improved SEO rankings and user engagement. It’s what your projects need to match the swift pace of today’s web.
    
2.  Delightful developer experience
    
    We’ve designed DatoCMS as a flexible platform to build precisely what you need. Our native GraphQL API and visual content modeling give you the freedom to build a content infrastructure that adapts to any scenario.
    
3.  A perfect blend of easy and powerful
    
    Our editing interface feels like a friendly wave from Wordpress, but with more power under the hood. Get creative with features like Blocks and Modular Content for a no-fuss, no-code page building experience that still packs a serious punch.

---

# When to choose DatoCMS over Storyblok?

Source [compare]: https://www.datocms.com/compare/datocms-vs-storyblok.md

Our customers prefer DatoCMS for its convenient scalability, unrivaled developer experience, and clean editing interface.

## DatoCMS vs. Storyblok: How we're different

DatoCMS is a great fit for tech teams seeking an **affordable headless solution**, that has also all the right tools to **build an editor-friendly space**.

Storyblok is an **enterprise** solution **mixing visual editing and headless**, which may **fail to meet specialized needs** of marketers and developers.

## At a glance comparison

### Who is it for

**DatoCMS:** Built **for tech teams** that want all the advantages of headless, while still prioritizing a **comfortable editor experience**.

**Storyblok:** Designed **specifically for marketers** who need to modernizing their tech stack, matching a headless architecture with visual editing.

### Pricing

**DatoCMS:** **Flexible pricing** that respects your budget, without the pressure to upgrade to an Enterprise-level plans too soon.

**Storyblok:** Steering **towards more lucrative customers** - the Enterprise tier is labeled as “Most popular” in their pricing page.

### Developer experience

**DatoCMS:** [Native GraphQL API](https://www.datocms.com/features/headless-cms-graphql.md) with extensive **documentation** and in-product **API Playground,** to ramp up and start deploying projects.

**Storyblok:** GraphQL API is **offered as a secondary option**, with a less straightforward documentation and a basic API playground.

### Asset Management

**DatoCMS:** The [Images API](https://www.datocms.com/features/images-api.md) comes out-of-the-box with **automatically optimized outputs** and **150+** powerful transformation options.

**Storyblok:** **Simple Images API** with 15 basic transformation options and limited automatic optimization with Webp detection.

**DatoCMS:** [**Native video streaming**](https://www.datocms.com/features/video-api.md) that's easy on the wallet, with adaptive outputs for any device.

**Storyblok:** You'll have to **build video streaming** from scratch, or wrangle by integrating a 3rd-party provider.

### Content Modeling

**DatoCMS:** **Organize content your way** with drag-and-drop sorting, tree-like collections, and handy single instance models.

**Storyblok:** The **flat content navigation** structure can be frustrating, often complicating the process of finding and accessing content.

**DatoCMS:** Use Blocks for repeatable page elements and develop a [**page building process**](https://www.datocms.com/features/dynamic-layouts.md) where editors can rearrange elements as needed with a simple drag & drop.

**Storyblok:** Use components to **create and nest repeatable page elements** without limits, fill them with content, and customize them as needed.

### Editor Experience

**DatoCMS:** **Real-time side by side previews**, so that editors can immediately see what their changes look like.

**Storyblok:** The **real-time visual editor** delivers an intuitive editing environment, tailored to enhance productivity for marketing teams.

**DatoCMS:** **Clean and approachable editing interface**, where every role can get involved and be productive from the very first day.

**Storyblok:** While visual editing may enhance the design experience, it tends to **compromise the usability of forms** for actually working with your content.

**DatoCMS:** Basic **collaboration features**, including workflows, presence indicators, and record locking.

**Storyblok:** **Well-designed collaboration capabilities**, including commenting, built-in notification system, and fine-grained approval workflows.

## Three reasons to consider DatoCMS

1.  Lightning-fast performance
    
    With a global CDN and optimized media delivery, your projects get an impressive boost in speed, plus improved SEO rankings and user engagement. It’s what your projects need to match the swift pace of today’s web.
    
2.  Delightful developer experience
    
    We’ve designed DatoCMS as a flexible platform to build precisely what you need. Our native GraphQL API and visual content modeling give you the freedom to build a content infrastructure that adapts to any scenario.
    
3.  A perfect blend of easy and powerful
    
    Our editing interface feels like a friendly wave from Wordpress, but with more power under the hood. Get creative with features like Blocks and Modular Content for a no-fuss, no-code page building experience that still packs a serious punch.

---

# When to choose DatoCMS over Strapi?

Source [compare]: https://www.datocms.com/compare/datocms-vs-strapi.md

Our customers prefer DatoCMS for its convenient scalability, unrivaled developer experience, and clean editing interface.

## DatoCMS vs. Strapi: How we're different

DatoCMS is built for SMEs and Agencies that want a **hassle-free jump to headless**. The SaaS, plug-and-play simplicity **provide** **devs with just what they need**, and no excess baggage.

Strapi is a good option for tech-savvy users that need an **open-source, self-hosted CMS** solution, albeit with the downsides of **slow setup and heavy-load maintenance.**

## At a glance comparison

### Who is it for

**DatoCMS:** Perfect for **SMBs and agencies** on the hunt for a no-sweat, cloud CMS. It's got all the goodies you need for your digital gigs.

**Strapi:** Aimedat **developers and tech-savvy users** looking for an open-source, self-hosted CMS, despite the potential complexities - though a cloud version has recently been introduced.

### Pricing

**DatoCMS:** **Predictable and affordable pricing**, ensuring no hidden costs catch you off guard. Test drive any feature with a free tier before making a decision.

**Strapi:** Self-hosting option offers either a Free or Enterprise tier with **no intermediate solutions**. Strapi Cloud's trial period is time-constrained, which might be **too limited for a thorough evaluation**.

### Setup and maintenance

**DatoCMS:** **Pure cloud** service, so your team can get on board and dive into project delivery with an **ideal setup from the get-go**.

**Strapi:** The core product being self-hosted implies a **noticeable ramp-up time** for setup and configuration before any real progress on a project can begin.

**DatoCMS:** **Automatic updates** keep your system up-to-date effortlessly, and you've got the option to review updates that may cause disruptions before they go live.

**Strapi:** Upgrading to new versions necessitates **manual intervention**, potentially introducing new API versions that carry the risk of **breaking changes**.

**DatoCMS:** One **central hub and app** to handle every aspect of your content.

**Strapi:** The extensive usage of **plugins and configurations** to operate the CMS can lead to unnecessary complexity and frustration.

### Developer experience

**DatoCMS:** Centers on offering a **true headless CMS solution**, with a significant focus on robust content delivery through APIs.

**Strapi:** Offer a **holistic approach** with capabilities such as user authentication and management, which may be **overkill** for projects that require simplicity over complexity.

**DatoCMS:** [**Native GraphQL APIs**](https://www.datocms.com/features/headless-cms-graphql.md) built to tackle even the most complex queries, providing your front-end with just the essential data.

**Strapi:** Native REST Content API, but **relegates GraphQL to a plugin**, resulting in more limited support. Allows developers to customize the backend code and the API responses.

**DatoCMS:** Promptly see the impact of any **changes made to the data model** with[Preview Environments](https://www.datocms.com/docs/scripting-migrations/introduction.md#whats-an-environment).

**Strapi:** No built-in tools for previewing, migrating, or deploying data changes across various environments, placing an **unnecessary burden on developers**.

**DatoCMS:** The [Images API](https://www.datocms.com/features/images-api.md)comes out-of-the-box with **automatically optimized outputs** and **150+** powerful transformation options.

**Strapi:** Offers **limited image handling** capabilities via unofficial plugins.

**DatoCMS:** [**Native video streaming**](https://www.datocms.com/features/video-api.md) that's easy on the wallet, with adaptive outputs for any device.

**Strapi:** You'll have to **build video streaming** from scratch, or wrangle by integrating a 3rd-party provider.

### Content Modeling

**DatoCMS:** Take advantage of [no-code content modeling](https://www.datocms.com/features/dynamic-layouts.md) to **build your models quickly and iterate** faster than ever.

**Strapi:** Users contend with a **stiff visual builder**, and while JavaScript schema modifications offer more autonomy, the trade-off in **time and technical demands** could be discouraging.

**DatoCMS:** Craft a **personalized content navigation** with drag-and-drop reordering, tree-like collections, single instance models, and emojis as well! 🎉

**Strapi:** The **flat content navigation** system leads to a needlessly convoluted experience when trying to find and access content.

### Editor Experience

**DatoCMS:** A **clean and inviting editing UI** that welcomes users of all roles to contribute immediately, equipped with structured text fields for **polished copywriting** and **embeddable components**.

**Strapi:** With its **complex and rigid editing UI**, plus no way to use components in rich-text fields, the platform **makes content editing harder** than it should be.

**DatoCMS:** Secure a worry-free editing environment with 40+ native **field validations**, side-by-side **draft previews**, and **versioning**.

**Strapi:** It offers **fewer validations** and a **simplified draft system** limited to unpublished content, with **no content history**.

**DatoCMS:** Optimize your global reach with **native localization capabilities**, featuring detailed per-field settings, locale-driven publishing, and translator-specific roles.

**Strapi:** Localization requires **yet another plugin**, and comes with more limited functionalities.

### Governance

**DatoCMS:** **Fine-grained permissions** designed to accommodate the needs of any project, offering the flexibility of **custom roles with all paid plans**.

**Strapi:** The platform offers custom roles and permissions **only on the Enterprise Edition**, and lacks **fine-tuned control**.

### Performance & Scalability

**DatoCMS:** As a SaaS backend service, DatoCMS manages scalability, **so developers can concentrate on the Frontend and UX**.

**Strapi:** Scalability hinges on the backend's web and database servers, **placing a burden on your IT** or forcing reliance on **expensive hosting services**.

### Support

**DatoCMS:** **Small but tight team of friendly experts** to help every customer get the most out of the product.

**Strapi:** To get any **real-time support**, you need to shell out for **Enterprise** - with the Community Edition, you're stuck with documentation and forum help.

## Three reasons to consider DatoCMS

1.  Lightning-fast performance
    
    With a global CDN and optimized media delivery, your projects get an impressive boost in speed, plus improved SEO rankings and user engagement. It’s what your projects need to match the swift pace of today’s web.
    
2.  Delightful developer experience
    
    We’ve designed DatoCMS as a flexible platform to build precisely what you need. Our native GraphQL API and visual content modeling give you the freedom to build a content infrastructure that adapts to any scenario.
    
3.  A perfect blend of easy and powerful
    
    Our editing interface feels like a friendly wave from Wordpress, but with more power under the hood. Get creative with features like Blocks and Modular Content for a no-fuss, no-code page building experience that still packs a serious punch.

---

# Next.js Concepts

Source [academy]: https://www.datocms.com/academy/frontend-frameworks/nextjs.md

Get a primer on some common concepts not directly related to the CMS that you'd come across when building frontend projects.

Note: For a comprehensive guide always refer to the [Next.js Docs](https://nextjs.org/docs) - We'll only be breezing over some concepts that have a lil something to do with concepts you might need when using a Headless CMS.

If you're new to Next.js and Headless CMS in general, our [Next.js Starter Kit](https://www.datocms.com/marketplace/starters/next-js-starter-kit.md) is an excellent place to start.

So, let's dip into the core concepts you'll come across in a super simple and digestible scope.

### What's `getStaticProps`?

If you have content that doesn’t change too often—like simple marketing pages like an `/about` or a `/contact`—`getStaticProps` would be your go-to. It fetches data at build time, generating fully static HTML for your pages. This means your users get ⚡ page loads, and your server gets a well-deserved break without having to clock in to overtime everytime that page needs rendering.

For example, imagine you’re fetching a list of blog posts from an API:

```typescript
export async function getStaticProps() {
  const res = await fetch('https://site-api.datocms.com/posts');
  const posts = await res.json();

  return {
    props: { posts },
  };
}
```

This function runs during the build process, so the data is pre-rendered into HTML. The result? Static pages that load almost instantly. And if you combine this with ISR (more on that later), you can even update your static content periodically without redeploying the entire site.

`getStaticProps` is a dope for any site that values speed and SEO without sacrificing dynamic content.

### What about `getServerSideProps`?

On the flipside, this one's all about real time data for every request.

If you need up-to-the-minute data for every request, `getServerSideProps` has your back. Unlike `getStaticProps`, this function runs on the server each time a page is requested, ensuring users see the most current content.

Take a dashboard as an example, and let's assume it's requesting user profile data that's always changing for whatever reason:

```typescript
export async function getServerSideProps() {
  const res = await fetch('https://site-api.datocms.com/user');
  const user = await res.json();

  return {
    props: { user },
  };
}
```

Here, the server fetches user-specific data for each request, allowing you to render content dynamically. It’s perfect for super frequently changing things like stock tickers, social feeds, dashboards, admin panels, or any app where content changes frequently.

The tradeoff? It’s slower than static generation, of course, since a server call happens on every request. But when real-time accuracy is non-negotiable, `getServerSideProps` lets you do what you gotta do.

### Ok, now what's `getStaticPaths`?

TLDR? Predefining some dynamic routes.

Dynamic routes in Next.js—like `/blog/[slug]`—are super convenient, but how do you tell Next.js which pages to pre-render? That’s where `getStaticPaths` comes in. It works alongside `getStaticProps` to generate paths for dynamic pages during the build process.

Get it? You got props. They need paths.

Here’s how it works:

```typescript
export async function getStaticPaths() {
  const res = await fetch('https://site-api.datocms.com/posts');
  const posts = await res.json();

  return {
    paths: posts.map((post) => ({ params: { slug: post.slug } })),
  };
}
```

This function ensures that all pages defined in the `paths` array are pre-rendered into static HTML. `getStaticPaths` is essential for projects with dynamic content where you're handling things in the CMS for every new record to have a new slug on publish, without having to refactor or rebuild things (depending on your build), and is relevant from simple blogs to packed e-commerce catalogs.

### What's this App Router everyone's talking about?

Ever used Next.js in the past and remember the simpler `pages/` times? Well, since Next.js 13, the App Router came about, and these days `app/` is favored over `src/`.

Next.js 13 introduced the App Router to boost how routing works by bringing powerful features like nested layouts, server components, and parallel routes.

With the App Router, you can now define shared layouts that automatically wrap all nested pages:

```arduino
app/
├── layout.js // App-wide layout
├── page.js
├── dashboard/
│   ├── layout.js // Nested layout for just the dashboard
│   ├── page.js
│   └── settings/
│       └── page.js
```

This approach simplifies complex apps by letting you define reusable layouts and isolate functionality. Add features like server-side rendering and static generation, and you’ve got a modern routing system designed for scale.

Now this is an ULTRA simplification, it's really worth [diving deep into their docs](https://nextjs.org/docs/app) to learn how the app router works.

### Next.js Server Components

Server Components are a realllly fun for working with React. Instead of shipping everything to the browser, they allow components to render on the server, sending only the final HTML to the client. This keeps JavaScript bundles small and pages crazy fast depending on the way they're set up.

For example, let’s say you have a product list that requires server-side fetching:

```typescript
export default async function ProductList() {
  const products = await fetch('https://site-api.datocms.com/products/pink-boots').then((res) => res.json());

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>{product.title}</li>
      ))}
    </ul>
  );
}
```

With server components, this logic runs entirely on the server, meaning no extra JavaScript gets sent to the browser. This approach is perfect for content-heavy apps like eCom shopfronts where performance matters.

### Next.js Middleware

From a marketing standpoint this is actually really cool. Next's middleware allows for all sorts of fun on the edge, like AB testing and redirects, without needing full-on server functions. Next.js Middleware lets you execute logic before a request is completed. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.

For example, if your eCom shop needs to have geo redirects to serve different content to users in different locations, you'd have a setup around these lines:

```typescript
import { NextResponse } from 'next/server';

export async function middleware(req) {
  const country = req.geo?.country || 'IT';

  if (country === 'DE') {
    return NextResponse.redirect(new URL('/de', req.url));
  }
}
```

Here, even though the default location/site is Italian, users sending a request from Germany are redirected to the German locale all before any other requests are completed.

### And what's this Edge Runtime?

The Edge Runtime in Next.js enables you to run server-side logic closer to your users. How close? ON THE EDGE CLOSE! Jokes aside, it'll run server side logic from the closest CDN edge node to the user. This means faster response times for often-heavy tasks like A/B testing, user authentication, or geo-based personalization.

For example, you can combine the Edge Runtime with Middleware for instant redirects:

```typescript
export const config = { runtime: 'edge' };

export default async function handler(req) {
  const country = req.geo?.country || 'DE';
  return new Response(`Hallochen!`);
}
```

Edge Runtime is lightweight, fast, and designed for modern web needs, making it really sweet to work with.

### What is ISR?

ISR or Incremental Static Regeneration was one of Next.js's dopest features for "mostly" Static Sites that needed periodic updates without redeploying the whole damn site.

What if you want the speed of static pages but the flexibility of dynamic updates? ISR allows you to update static content at specific intervals or on demand—no need to redeploy your site. So if your entire site is Static but you post blog posts every now and then, ISR is what would let your site update every `x` hours without you needing to deploy everything.

```typescript
export async function getStaticProps() {
  const res = await fetch('https://site-api.datocms.com/posts');
  const posts = await res.json();

  return {
    props: { posts },
    revalidate: 3600,
  };
}
```

For example, if you got a blog post, a little snippet like that will regenerate all your blog posts (only) every 1 hour, letting your site refresh when there's new content but leaving the rest of it untouched.

*PS: while ISR is cool, check out* [*our Cache Tags*](https://www.datocms.com/docs/content-delivery-api/cache-tags.md) *which allow you to simply tag webpages with unique identifiers, so when the content from the CMS is updated, these tags can trigger an immediate and precise cache invalidation only for the pages that actually include that content, and need to be regenerated.*

### What's `next/image`?

Images can make or break your site’s performance if you're not optimizing them well. That’s why Next.js’s `next/image` is such a sweet feature in Next. It handles stuff like resizing, lazy loading, and modern formats like WebP—all automatically and out of the box. You don’t need third-party DAMs or anything of the sort.

Simply slap some params to your images when dropping them in:

```typescript
import Image from 'next/image';

<Image
  src="/shawarma.jpg"
  alt="Shawarma"
  width={800}
  height={600}
  ...
/>
```

It even supports dynamic imports for CMS-driven content in case you're not using Dato's inbuilt optimizations for whatever reason 😛.

### What's `next/seo`?

Another cool feature out of the box for making websites easier to build. Most Headless CMS (yours truly very much excluded 💁‍♀️) don't offer strong SEO-focused features, in which case `next/seo` is a really fun solution.

We all know that SEO is vital for visibility (I mean, that's the reason behind this whole Academy), but managing metadata and OG tags manually is a f\*n chooooore. Enter `next/seo`, a package designed to simplify SEO in Next.js. It helps you manage titles, descriptions, canonical URLs, and even social media metadata without the boilerplate.

The implementation is super straight forward too, if you're not just relying on site-wide fallbacks in your config, then you can add in metadata per page via the `<NextSEO />` component

```typescript
import { NextSeo } from 'next-seo';

<NextSeo
  title="Best Shawarmas in Berlin"
  description="If the toum no gucci, the shawarma no gucci."
  canonical="https://berlin-shawarmas.com"
  openGraph={{
    url: 'https://berlin-shawarmas.com',
    title: 'Best Shawarmas in Berlin',
    description: 'If the toum no gucci, the shawarma no gucci.',
  }}
/>
```

### What is `next/i18n`?

Another cool feature for international sites, especially when your CMS doesn't have strong localization built in (don't be looking at us, we got this 💅).

Building multilingual websites used to be (and still can be) a pain, but Next.js has simplified the process with its built-in i18n support. This feature handles locale detection, routing, and translations seamlessly.

```typescript
module.exports = {
  i18n: {
    locales: ['en', 'it', 'de'],
    defaultLocale: 'it',
  },
};
```

Next.js automatically routes users to the correct locale, like `/fr` for French or `/de` for German, provided you have them set up. You can even integrate it with libraries like `next-translate` for more advanced use cases. Whether you’re building a global e-commerce site or a multilingual blog, `next/i18n` makes localization feel effortless.

Remember the example of DE from when we were talking about middleware? Yeah? i18n + middleware is such a sweet, sweet combo :chef-kiss:.

### What's `next/script`?

We ain't calling out anyone specific, but so many sites see reallllllly see terrible performance when loading 50 shades of 3rd party scripts for things like analytics and tracking and personalization and ads and heaven knows what else.

So, Next.js has `next/script`, to let you load scripts intelligently. Will they fix terrible-to-begin-with-scripts that are heavy? No. But they will let you load them using smarter strategies like `lazyOnload` or `beforeInteractive` to salvage whatever performance gains you can without dropping the script.

For example:

```typescript
import Script from 'next/script';

<Script
  src="https://example.com/script.js"
  strategy="lazyOnload"
/>
```

will ensure that scripts only load when they're needed.

---

# Astro Concepts

Source [academy]: https://www.datocms.com/academy/frontend-frameworks/astro.md

Get a primer on some common concepts not directly related to the CMS that you'd come across when building frontend projects.

Note: For a comprehensive guide always refer to the [Astro Docs](https://docs.astro.build/en/getting-started/) - We'll only be breezing over some concepts that have a lil something to do with concepts you might need when using a Headless CMS.

Now, it's worth noting that we're pretty big fans of Astro at Dato - in fact, we moved our entire website to it quite recently (well, recently if you're here beginning of '25). We wrote [about all the reasons behind it](https://www.datocms.com/blog/why-we-switched-to-astro.md), so definitely warrants a dive!

So. If you’re here, you’ve probably heard the buzz about Astro's Island Architecture or its ability to ship zero JavaScript by default. It’s not just hype—Astro’s approach to building for the web is unique, performant, and downright fun. In many ways it brings back the joys of webdev in the early 2010s, but that's just our opinion.

So let's explore some of the key concepts that make Astro so special, keeping things simple and practical. If you need a deeper dive, you can always check out the Astro Docs. But for now, let’s look at some core concepts worth knowing about.

### WTF are Astro Islands?

Astro’s Island Architecture is kind of genius. Traditional SPAs ship a ton of JavaScript up front, even for static parts of the site. Astro flips that logic on its head: only the dynamic, interactive parts of your site get JavaScript—and only when they need it. Otherwise you're just shipping compiled HTML and CSS to keep things ultra light.

Let's stretch the islands analogy. Think of your page as an ocean, and the interactive components (like a carousel or a search bar) as tiny little islands. Astro renders everything server-side, leaving plain HTML for most of your site, and hydrates just those "islands" of interactivity in the browser. The result? A blazing-fast site with the minimal JavaScript your users deserve.

PS: We "stress-tested" 4 frameworks to see how they performed head to head - so check out our dive into how [Astro performed when building a static site with 10K blog posts](https://www.datocms.com/blog/comparing-js-frameworks-for-content-heavy-sites.md).

### What's "Zero JS by Default"?

Astro ships zero JavaScript unless you tell it otherwise. This means your site is as lean as possible by default. If you don’t need interactivity, Astro won’t make your users download a single kb of unnecessary JS.

Need an interactive component? Wrap it in a framework like React, Svelte, or Vue, and Astro will hydrate it *only when it’s needed*. Want even more control? You can define hydration strategies like `"load"`, `"idle"`, or `"visible"` to decide when and how the JavaScript gets loaded.

```typescript
<Button client:visible />
```

For example 👆 Astro will only hydrate the button when it's scrolled into view thanks to the `client:visible` directive.

### Partial Hydration

(This is me being cheeky with my SEO stuffing 🤭)

Yes. This is just more of "Zero JS by default".

Anyways.

### Astro's Collections API

If you’re working with a lot of content—think 1000s of blog posts or products—the Collections API is incredible. It lets you define schemas, validate content, and organize data effortlessly.

```typescript
import { defineCollection } from 'astro:content';

export const collections = {
  blog: defineCollection({
    schema: {
      title: "string",
      date: "date",
      description: "string",
    },
  }),
};
```

The Collections API allows for content to be structured and validated, and not just stored.

### What is Astro IntelliSense?

As much as it sounds like a bloated feature in some AI-powered washing machine that needs an app and [uses up 3GB+ data a day because hey, wash cycles are DLCs now](https://www.reddit.com/r/gadgets/comments/196bb6c/your_washing_machine_could_be_sending_37_gb_of/), Astro IntelliSense is anything but.

It's actually useful.

IntelliSense provides autocompletion and validation while writing content. If you’ve defined schemas in `astro:content`, IntelliSense suggests field names, types, and values directly in your editor.

For example, when creating a new Markdown blog post, you’ll get suggestions for required frontmatter fields like `title` or `date`. It’s like having a CMS validation layer directly in your code editor, reducing errors and speeding up your workflow.

### Frontmatter in Astro

You're probably familiar with how metadata is handled usually in frontmatter, but Astro does things slightly differently. Whether you’re working with Markdown files or `.astro` components, frontmatter lets you define page-specific properties like titles, descriptions, and dates.

In Markdown, it looks like this:

```markdown
---
title: "Am I having a midlife crisis?"
date: 2025-01-10
description: "I think so. I went ceramic paining."
---
```

In `.astro` files, you can use a `<script>` block for the same purpose:

```typescript
---
export const frontmatter = {
  title: "Am I having a midlife crisis?",
  description: "I think so. I went ceramic paining.",
};
---
```

Frontmatter here isn’t just about organization—it powers dynamic routing, SEO metadata, and even page templates. It’s the connection that keeps your content structured and flexible.

### What are Astro Slots?

Slots are Astro’s way of making reusable components even more powerful. A slot is a placeholder that allows you to inject content into a component without rigidly defining which elements need to be in there every time.

For example, imagine a `Card.astro` component:

```typescript
<article>
  <slot />
</article>
```

You can use this component and define the content dynamically:

```typescript
<Card>
  <h2>Header</h2>
  <p>Give it a spin</p>
</Card>
```

Slots keep your code modular and flexible, perfect for building reusable design systems.

### Astro Aliases

With aliases, you can ditch long relative paths and make your imports waaay more readable (out of the box). Instead of:

```typescript
import Component from '../../../../../../../../../../components/Button.astro';
```

just set up an alias

```typescript
alias: {
  '@components': './src/components',
}
```

And then all you'll need to do is a simple

```typescript
import Button from '@components/Button.astro';
```

Easy.

### Astro Adapters

One of the standout features of Astro is its... 🥁... Adaptability—*sorry*. Astro adapters allow you to deploy your site to almost any platform, from serverless environments like Vercel and Netlify to edge platforms like CF Workers or traditional hosting setups. These adapters take care of the heavy lifting, ensuring your Astro project runs smoothly on your preferred hosting solution.

Here’s how it works. Astro generates HTML by default. But what if you want dynamic server-side rendering (SSR) or specific configurations for a platform? That’s where adapters come in. Each adapter customizes Astro’s output to match the requirements of your hosting platform.

Just `npm` an adapter like `@astrojs/netlify` and update your config and deploy!

Astro will generate output optimized for the deployment platform you're using. Adapters let you focus on your content instead of worrying about platform-specific quirks.

Need edge rendering for global users? Use the Cloudflare Workers adapter.

Running a simple blog? Stick with the SSG adapter.

Building dynamic dashboards? Go with the Vercel or Node.js adapter for SSR.

### Astro Scripts

Astro’s approach to scripts is all about balance: you get the power of JavaScript without drowning your users in unnecessary downloads. Whether you need inline scripts for performance, external scripts for modular stuff, or conditional loading for interactivity, Astro gives you full control.

Astro makes it easy to define when and how scripts load, thanks to its flexible directives to maintain its' zero-JS approach. Some cool examples are:

**`client:load`**: Loads the script immediately after the page loads.

**`client:idle`**: Waits until the browser is idle to load the script.

**`client:visible`**: Loads the script when the component is visible in the viewport.

### Looking at `astro:actions`

Astro’s `astro:actions` feature allows you to handle forms on the server without needing client-side JavaScript. You can define custom actions in your server endpoints, which are triggered when a form is submitted. This keeps your forms lightweight and ensures the UX remains seamless.

```typescript
<form method="POST" action="/api/contact">
  <input type="text" name="name" required />
  <button type="submit">Submit</button>
</form>
```

For example, if you’re building a contact form, you can process the form submission directly on the server for better performance and security, and return feedback without needing a client-side framework.

### Understanding `astro:assets`

The `astro:assets` module allows you to optimize and manage images dynamically without needing any external service like a DAM to life the load on your optimization needs, which is nifty if you happen to be using a CMS that doesn't handle this out of the box (cough\* cough\* [we do](https://www.datocms.com/docs/asset-api/images.md)).

Whether you need to resize, crop, or convert images to different formats, `astro:assets` handles it all during build time, ensuring your end website remains fast and efficient.

---

# React Concepts

Source [academy]: https://www.datocms.com/academy/frontend-frameworks/react.md

Get a primer on some common concepts not directly related to the CMS that you'd come across when building frontend projects.

Is React ABSOLUTELY NECESSARY to work with DatoCMS? No. But it is the most commonly used JS framework among our users. If you're not into React, but prefer Vue, Rust, or anything else, that's perfectly fine. For now, let's (shallow, like, super shallow) dive into the core React concepts, patterns, and optimizations that can support your projects.

Note: For a comprehensive guide always refer to the[React Docs](https://react.dev/learn) - We'll only be breezing over some concepts that have a lil something to do with concepts you might need when using a Headless CMS, and we’re assuming you’re somewhat familiar with React to get the terminology.

And if you’re looking for the core resources to hook up React with DatoCMS, our [react-datocms package of components & utilities](https://github.com/datocms/react-datocms), and [docs on React UI Components](https://www.datocms.com/docs/plugin-sdk/react-datocms-ui.md) are a great place to start.

## **WTF are React Hooks?**

React Hooks legit is like the heart of what makes React so functional and what makes functional components so damn powerful. Instead of writing classes and lifecycle methods, Hooks let you handle state (useState), effects (useEffect), and context (useContext) in a much cleaner way. More on those later.

When working with CMS content, Hooks streamline the process of fetching data, handling forms, and managing side effects. Imagine pulling data from DatoCMS: instead of managing everything with a class component, you can use useEffect for fetching, useState for the loading and error states, and useMemo to optimize the response—all within a few lines of code.

Personally I’ve always enjoyed this Treehouse analogy on [understanding Hooks](https://www.reddit.com/r/react/comments/11ftu0p/what_are_hooks/jaliuen/) from this older Reddit thread.

## **Understanding React Router**

Navigating between pages on a single-page application (SPA) needs to be fast and seamless, and that’s exactly what React Router provides.

It lets you switch between different "pages" without triggering a full page reload. If your app has multiple routes—like `/blog` for listing posts and `/blog/slug` for individual posts—React Router keeps the experience smooth. Dynamic routing is especially useful when dealing with CMS-driven pages. You can grab URL parameters (like slugs) using the `useParams` hook and use them to fetch specific data from your CMS.

This way, implementing React Router lets your components stay small and focused, with routes handling the heavy lifting.

## **What are React Portals**

Sometimes your component tree miiiiight feel a touch restrictive. Maybe you’re building a modal, a floating CTA, or a tooltip that needs to live at the very top of the DOM for proper styling and positioning.

That’s where React Portals come in—they let you render components outside of the parent DOM node while keeping everything functionally intact.

For example, let’s say you have a "Subscribe" modal that should appear across all pages, but you don’t want it cluttering up your component tree. By rendering it through a portal, you can keep your DOM structure clean while still managing its behavior through state and props. Portals are perfect for anything that needs to overlay the main content, and they keep your layout organized while avoiding common styling headaches.

## **React Suspense**

React.Suspense is for when you need to handle asynchronous operations and loading states elegantly. Instead of manually managing loaders or fallback components, Suspense can wrap sections of your app and handle what gets displayed while data or components are loading.

In a Headless CMS project, this is great for deferring the loading of non-essential content like related blog posts or carousels. You can show a skeleton loader, a spinner, or even a custom fallback component until the content is ready. This keeps the UI smooth and prevents jarring "content flashes" as data is loaded dynamically.

## **React Table**

When it comes to rendering large tables of CMS data—think reports, directories, or product listings for massive eCommerce apps—react-table is a lifesaver.

It provides out-of-the-box functionality for sorting, filtering, pagination, and virtual scrolling for those massive datasets.

Imagine you’re building an admin dashboard that lists hundreds of articles fetched from your Headless CMS. Instead of reinventing the wheel, react-table makes it simple to create a responsive, feature-rich table that won’t clog up the browser. Its API is flexible, allowing you to customize column headers, cell formatting, and more.

B-Y-O-UI though.

## **PropTypes**

I’d say this is a blessing for all y’all NOT using TypeScript but still want to somehow enforce a level of type safety in your components. By specifying expected prop types for your components, you can catch errors before they hit production.

For instance, if you have a BlogCard component that expects title, description, and author props, defining them with PropTypes.string.isRequired ensures that your component won’t accidentally receive the wrong type of data from your CMS. While it’s not as robust as TypeScript, it’s a simple way to add some guardrails to your React app.

## **React Dropzone**

Running an app that lets users dump images/files or upload anything else? react-dropzone simplifies the process of adding simple drag-and-drop file uploads. It abstracts away the complexity of handling file inputs and lets users upload files by dragging them directly into the designated area.

Imagine you’re building a CMS-powered content creator tool that allows users to upload images for their articles. Instead of dealing with native file input quirks, react-dropzone gives you a customizable drop area where users can easily upload files.

Bonus: it supports drag-and-drop events, file previews, and validation rules out of the box.

## **What is** **`useState`**

`useState` is the bread and butter of React development to manage state locally. It’s what makes your components interactive and dynamic.

At its core, it’s a simple way to manage values that change over time—things like form inputs, toggles, and counters. When working with a Headless CMS, `useState` can be super useful for things like handling user filters or toggling between different views of your fetched data.

Take the example of the classic "Load More" button, assuming that your blog is loading 10 posts at a time. Each time the button is clicked, the page count increments and fetches more data. `useState` is what allows you to track that page count and trigger a re-render seamlessly (without having to explicitly introduce page based pagination of 10 posts per /p/x.

That being said, `useState` is ideal for handling individual pieces of state at the component level. But if your state starts getting more complex—like sharing data across different parts of the app—you might need to bring in a context API or a more advanced state management solution.

## **What is** **`useEffect`**

You’re probably familiar with how React is all about keeping things declarative—components should be predictable and purely dependent on their inputs. But every now and then, some operations, like fetching data from your CMS, are side effects that don’t necessarily fit that mold.

This is where `useEffect` comes in. This hook chases after your component renders and handles the heavy lifting on things like data fetching and subscriptions.

Let’s say you’re building a page that displays rich text from DatoCMS. You can use `useEffect` to fetch the data as soon as the component mounts, ensuring the content appears as soon as the page loads. Need to update the page title for SEO? `useEffect` can handle that too. One key thing to remember is that `useEffect` isn’t just "set and forget." If your effect involves async calls, always clean it up by returning a cleanup function—especially if you're dealing with things like subscriptions or timeouts. This prevents memory leaks and keeps your app performant.

To help illustrate what I mean by that, here’s a simple snippet from GPT once I fed that paragraph in:

```jsx
import React, { useState, useEffect } from 'react';

function BlogPostPage({ slug }) {
  const [postData, setPostData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let isMounted = true; // To avoid setting state after unmount

    const fetchData = async () => {
      try {
        const response = await fetch(`/api/dato-post?slug=${slug}`);
        const data = await response.json();

        if (isMounted) {
          setPostData(data);
          document.title = `${data.title} | My Blog`; // SEO title update
          setLoading(false);
        }
      } catch (error) {
        console.error('Error fetching post:', error);
        if (isMounted) setLoading(false);
      }
    };

    fetchData();

    return () => {
      isMounted = false; // Cleanup: prevent state updates if unmounted
    };
  }, [slug]); // Dependency array ensures this runs when the slug changes

  if (loading) {
    return <p>Loading post...</p>;
  }

  return (
    <div>
      <h1>{postData.title}</h1>
      <p>{postData.description}</p>
      <div dangerouslySetInnerHTML={{ __html: postData.content }} />
    </div>
  );
}

export default BlogPostPage;
```

## **What is** **`useRef`**

Think of `useRef` as a React sticky note I suppose—it remembers values between renders without triggering re-renders.

This is perfect for storing mutable data, DOM elements, or even functions. If you’ve ever needed to focus an input field or keep track of a scroll position without resetting it, `useRef` is your friend.

In a CMS-powered app, you might use `useRef` to create an interactive image gallery where you need to keep track of which slide the user is on, even as they navigate away and back again. Or maybe you’re building a scrolling animation that needs precise measurements of a DOM element—`useRef` allows you to reference that element directly without rerendering the component. The real magic is how out-of-the-way and unobtrusive `useRef` is.

Since it doesn’t trigger renders, you can store values like cached API responses and timers without affecting the component’s lifecycle. Just don’t try to use it for state management.

## **What is** **`useCallback`**

When you start passing functions down as props to child components, things can get messy—especially if the function changes with every render. This can lead to unnecessary re-renders and sluggish performance. `useCallback` fixes that by memoizing the function, ensuring that the same instance is reused unless its dependencies change.

In Headless CMS apps, this is dope for things like dynamic lists. Picture a product listing page with filters and sort options. Every time the user changes the filters, the `onChange` function can be wrapped in `useCallback` so that the component doesn’t completely re-render the entire list of items and simply “reflects the changes”. This makes the UI feel snappier and prevents React from doing extra work behind the scenes.

## **What is** **`useMemo`**

React memo is like bubble wrap around your components—it protects them from re-rendering when they don’t need to. If a component’s props haven’t changed, memo ensures that React reuses the last render result instead of recalculating everything which is pretty nifty in apps with tons of components with varying levels of changes.

For example, if you’re rendering a large list of CMS content and some parts of the UI (like a sidebar) don’t change as often, wrapping the sidebar in React.memo can save you tons of unnecessary work by not having that sidebar constantly re-render only to display the same stuff over and over again.

This drastically improves performance, especially if you’re working with large datasets. `useMemo` is also helpful for creating complex derived data, like formatting dates or calculating aggregates. Just be careful not to overuse it—sometimes, your optimizations might be solving a problem you don’t actually have. Classic overengineering.

---

# Headless CMS Selection Criteria

Source [academy]: https://www.datocms.com/academy/headless-cms/headless-cms-selection-criteria.md

Learn the concepts, applications, and criteria that go into making the move towards headless content management

## TLDR

-   Each Headless CMS is slightly different, but there's 6 areas to consider when evaluating them in order to make the right choice.
-   **Core capabilites**: Amongst many other features, the CMS should support the basics for modern content management such as projects, environments, localization, flexible content modelling, and asset management.
    
-   **Developer Experience**: A strong Headless CMS places a good DX at the center of it's focus, and provides features around CLIs, robust APIs, up-to-date libraries, API playgrounds, migration tools, and granular tokens.
-   **Editor Experience**: Similarly, editors should look out for a good UX around features like text and image management, SEO, live previews, scheduled publishing, versioning, collaboration, and custom workflows.
    
-   **Extensibility and Integrations**: Since modern digital experiences use multiple tools and APIs to build unique customer experiences, the CMS should be able to easily integrate with other 3rd party Martech tools, APIs, and other services.
-   **Security, Governance, and Permissions**: To ensure the integrity of the project, the CMS should allow for custom and granular access control, tokens, and have the relevant compliance checks in place for it's architecture.
    
-   **Support, Community, and Documentation**: The vendor should have an active community of users, well documented APIs, and an active combination of assisted and self-serve support.
    

Now that we've gone over the high-level understanding required when deciding to opt for a Headless CMS, let's cover some specific capabilities you should keep an eye out for to make the right decision.

## **Core Capabilities**

A robust CMS should bring out key features out-of-the-box to set you up with a good foundation. Normally, Headless CMS should allow for multiple teams to work on multiple projects, and provide certain features enabling this to happen smoothly:

-   [**Projects**](https://www.datocms.com/docs/general-concepts/project-account-usages.md): The ability to have more than just one project for a variety of reasons, whether for websites, apps, or other digital entities.
-   [**Environments**](https://www.datocms.com/marketplace/starters.md): More than just one \`main\` environment, allowing for developers to create sandbox environments for testing schema changes, or other big experiments.
    
-   [**Locales**](https://www.datocms.com/docs/general-concepts/localization.md): The ability to create content serving different markets, ideally not only via different languages, but via locale-specific configurations (i.e., the ability to serve different Spanish content to es-ES and es-MX, rather than just ES).
-   [**Powerful content modeling**](https://www.datocms.com/docs/content-modelling.md): The flexibility to create and define your schema as you see fit, with a variety of model and field types including model collections, single-instance models, tree-like models, and blocks.
    
-   [**Field types**](https://www.datocms.com/docs/content-modelling.md): Schemas should reflect exactly what you’re trying to accomplish without messy hacks or workarounds. A rich variety of field types (like text, image, JSON, location, boolean, etc.) empowers you to define the ideal schema for your project(s).
-   [**Asset Management**](https://www.datocms.com/docs/content-delivery-api/images-and-videos.md): Your CMS should come with robust DAM capabilities out of the box (as well as the ability to integrate to others). This should not only facilitate the CRUD operations of images, videos, and documents, but also provide optimization features for better bandwidth management, performance, and SEO.
    

## **Developer Experience**

The DX is among the most critical aspects of selecting a Headless CMS – it should empower the devs to build better and faster rather than getting them stuck with inflexible technologies and ancient workflows. Here’s some of our considerations on what goes into a good DX for Headless CMS:

-   [**Starter Projects**](https://www.datocms.com/marketplace/starters.md): Fully-fledged starter projects help developers get up to speed with new systems. Typically, having a ready to use CMS project for common use cases like websites and portfolios help get up to speed with the APIs, UIs, and other tools, making migrations and new projects go smoother.
-   [**CLIs**](https://www.datocms.com/docs/cli.md): Command Line Interface (CLIs) functionalities allow developers to rapidly execute and automate CMS tasks from the command line.
    
-   **Popular integration libraries**: Given most modern development is within the JS ecosystem, having a CMS that comes with ready to use libraries for popular frameworks like [React](https://www.datocms.com/docs.md), [Next](https://www.datocms.com/docs/next-js.md), [Remix](https://www.datocms.com/docs/react-router.md), and [Svelte](https://www.datocms.com/docs/svelte.md) can speed up development time considerably.
-   [**Plugin SDK and UI ecosystems**](https://www.datocms.com/docs/plugin-sdk/introduction.md): Devs can develop custom plugins with ease using Software Development Kits (SDKs) and UI system, extending the CMS functionality to suit your specific requirements around specific 3rd party tools, workflows, or processes.
    
-   [**API Playgrounds**](https://www.datocms.com/docs/real-time-updates-api.md): An inbuilt API playground lets developers test, iterate, and experiment on the overall content query performance and complexity in a safe environment before implementing anything in the code base.
-   [**Migration tools and scripts**](https://www.datocms.com/docs/scripting-migrations/scripting-migrations-with-the-datocms-cli.md): It’s never easy to migrate years of content and digital projects from one CMS to another considering how much of a cornerstone the CMS is in one’s digital stack. A good CMS should have multiple import and export options, migration tools, and easy to reference scripts (preferably in open file formats that aren’t proprietary) to make the transition smoother.
    
-   [**API Tokens with granular permissions**](https://www.datocms.com/docs/content-management-api/resources/access-token.md): Security and content access are critical to larger projects. CMSs should allow the generation of API tokens with precise permissions, granting secure access to specific data and actions, enhancing data security.
-   [**Content Delivery APIs**](https://www.datocms.com/docs/content-delivery-api.md): A no-brainer, but it’s critical that the CMS should provide well-documented and robust APIs to easily query all the content needed from the CMS into the end projects.
    
-   [**Content Management APIs**](https://www.datocms.com/docs/content-management-api.md): As an additional functionality, a CMS should also have a strong content management API to allow for the programmatic creation, modification, and removal of content via API, for cases where manually created content via a UI isn’t the right approach.
-   [**Content Preview APIs**](https://www.datocms.com/features/real-time-api.md): For better project management and content validation, having a content preview API to see and experience changes in real-time before making alterations to the repo makes a big difference.
    
-   [**Real Time Update APIs**](https://www.datocms.com/docs/real-time-updates-api.md): For advanced use-cases where end-users need to consume data in real time as changes happen (think stock prices, live news, or sports updates), a CMS should provide a subscriptions API for the frontend to subscribe to changes, without needing to push a new build every time content is added or updated.
-   [**Images APIs**](https://www.datocms.com/docs/content-delivery-api/images-and-videos.md): A good Images API to efficiently handle and deliver images, optimize website performance, and ensure a smooth user experience is a great feature to ensure you have access to. Image manipulation, optimization and format conversions via URL params are typical enrichments you should look out for.
    
-   [**Video Streaming APIs**](https://www.datocms.com/docs/content-delivery-api/images-and-videos.md): If your use-case requires you to deliver high-quality video streaming with adaptive bitrate, adjusting video quality based on users' network conditions for optimal viewing, then your CMS should either provide a video streaming API, or have well documented alternatives for you to integrate.
-   [**User Management APIs**](https://www.datocms.com/docs/content-management-api/resources/role.md): Aside from the content itself, a CMS should allow for programmatic user management, enabling efficient user administration and project management.
    
-   [**CDNs**](https://www.datocms.com/features/worldwide-cdn.md): And lastly, all these capabilities should be scalable for you depending on where your customers are. Ensure fast and reliable content delivery worldwide by reducing latency and improving user experience by using a CMS with a strong CDN network and caching policy in place.
    

## **Editor Experience and Workflows**

Similarly, the editorial experience is as important to the successful implementation of a new CMS as is the DX. For editors, there’s a much wider range of considerations when selecting a system.

-   [**Structured rich-text editing**](https://www.datocms.com/docs/content-modelling/structured-text.md): A strong rich-text editor is both intuitive and powerful, letting your content editors easily write and format text, add images, links, custom blocks, and more. If they're coming from Wordpress, Wix, SquareSpace, or similar, they'll need a structured text editor that gives them all the capabilities they're used to out-of-the-box.
-   [**Image editing**](https://www.datocms.com/docs/asset-api/images.md): Since most CMSs would come with inbuilt Digital Asset Management (DAM) solutions, it's important to see how they differentiate between one another. A solid approach should be to choose one that let's you edit and optimize images directly within the CMS, saving time and ensuring visual consistency across your website or app.
    
-   [**SEO and Social metadata management**](https://www.datocms.com/docs/content-modelling/seo-fields.md): Even though Headless CMSs can do more than websites, a website is very much the most common use-case, whether for publications, eCommerce, or something else. Either natively or via plugins, a good CMS should let you customize SEO metadata and social media sharing settings for each piece of content, improving your website's discoverability and user engagement.
-   [**Live previews and page building**](https://www.datocms.com/blog/why-we-switched-to-astro.md): Similar to the previous point, a CMS focused on making better websites should integrate with popular frontend deployment services natively or offer the ability to build custom previews, giving editors an opportunity to see their content before it goes live. As an added bonus, having options to build pages via modular content blocks make it much easier for editors to build, edit, and preview pages.
    
-   [**Scheduled publishing**](https://www.datocms.com/docs/general-concepts/scheduled-publishing-unpublishing.md): Plan and automating content publication (and unpublishing) with scheduled publishing helps ensure timely updates and saves effort in manually pushing content live. Scheduled publishing should be available for single pieces of content, or collections scheduled together.
-   [**Content validations**](https://www.datocms.com/docs/content-modelling/validations.md): Enforce content consistency and quality with content validation rules via the CMS, ensuring that content meets specified criteria before publication.
    
-   [**Localizations and translations**](https://www.datocms.com/docs/general-concepts/localization.md): A strong Headless CMS should allow for granular locale-based publishing, where translations can be easily entered in (manually or programmatically) for any number of locales. A good way to ensure this is to check whether your vendor would only support, for example, creating content in English and French. Or whether they'd allow you to go even more granular and extend that to English (US), English (Australia), French (France), and French (Switzerland), for instance.
-   [**Content versioning**](https://www.datocms.com/docs/general-concepts/versioning.md): Versions let you track and review content changes over time with a comprehensive content history log, facilitating version control and content auditing. Typically a CMS should allow you to "rollback" or revert back to previous versions of a record should you decide that an earlier version was more relevant or accurate.
    
-   [**References and Links**](https://www.datocms.com/docs/content-modelling/links.md): CMSs should let you establish relationships between content items using links/reference fields, fostering a structured content architecture and cross-referencing, rather than having to manually enter related data for each entry. For example, rather than each blog post having to fill out the author information with each entry, a post should simply be linked to an author.
-   [**Real-time collaboration**](https://www.datocms.com/docs/general-concepts/collaboration-features.md): Collaboration means different things to different teams. At the basic level, a CMS should allow for you to know who's interacting with content in real-time, with multiplayer presence showing who's active and contributing to the editorial process.
    
-   [**Custom workflows**](https://www.datocms.com/docs/general-concepts/workflows.md): Streamline content approval and publication processes with customizable editorial workflows, ensuring a smooth content lifecycle management.
    

## **Extensibility and Integrations**

Especially when building connected digital experiences using multiple tools and services, assessing a CMS’s extensibility and integration capabilities is crucial. These factors determine how well the CMS can adapt to your evolving digital ecosystem and seamlessly connect with other tools and systems.

Your CMS should offer a [strong plugin ecosystem](https://www.datocms.com/marketplace/plugins.md), or the ability to create and host your own. These can range from simpler in-UI enhancements like SEO metadata previews, to something more advanced like selecting a product from an eCommerce catalog API via your PIM.

Check for a range of pre-built integrations with popular tools like marketing automation platforms, CRMs, e-commerce APIs, and analytics tools. For integrations that aren’t “out of the box”, the CMS should offer robust APIs for connecting with external systems.

## **Security, Governance, and Permissions**

We’ve established that your CMS is a critical component of your digital stack, and as such, should ensure that all security and compliance measures are appropriate to your use-case. [A secure CMS safeguards content, information, and access, mitigating potential risks and breaches](https://www.datocms.com/enterprise-headless-cms.md). Commonly, it’s worth looking into making sure that your CMS vendor has a comprehensive process in place for backups, scaling, audit logs, and encryption.

While it’s good to ensure they’re compliant with global security standards like ISO and GDPR, it’s equally worth exploring their in-product capabilities for granular access control for both, users, and APIs.

## **Support, Community, and Documentation**

Lastly, always look for signs of an active product community! If there’s a [strong user community](https://www.datocms.com/slack.md), the chances you’re going to get a better experience are much higher. Good CMSs should have up-to-date information on their API docs, [regularly updated changelogs](https://www.datocms.com/product-updates.md), [engaged support communities](https://community.datocms.com/?_gl=1*ges16*_gcl_au*MTMyNDUzMDM2My4xNzAxODcxMzA2&_ga=2.222144088.1308849776.1705310874-1791775518.1701871306), and a range of guides to help users get up to speed with the system.

To get a complete overview of a Headless CMS like DatoCMS checks these boxes, explore a [full list of our features](https://www.datocms.com/pricing.md).

---

# Deployments

Source [academy]: https://www.datocms.com/academy/modern-web-development/deployments.md

Understand some common use-cases, APls, and frameworks that go into building modern web experiences with Headless CMS

Cool, so you’ve got your CMS sorted, you’re creating tons of content, and you’ve built this incredible website or web-app using NextJS, React Native, Astro, or whatever else you chose to work with. It’s all sitting in your repo. All that’s missing is to deploy this and share it with the rest of the world so it’s not just a pretty thing collecting dust on localhost.

Even if it’s a cool side-project, we don’t want another one of these now, do we 👇

(Image content)

## Traditional web deployments

Traditional web deployments have been foundational in shaping today’s web hosting landscape. Originally, web hosting was centered around self-run physical servers (shoutout to the bare metal that’s still keeping a big chunk of the web going!), either through dedicated hosting for individual websites or shared hosting for multiple sites. This approach, while simple, often struggled with scalability.

cPanel stands out as a significant tool from this era. It simplified website and hosting account management with a user-friendly graphical interface, offering functionalities like domain management and web app installation (#SoftaculousMemories). Despite advancements in web hosting, cPanel’s comprehensive features maintain its relevance in many hosting environments.

Heroku is another behemoth that represents an early evolution in web-app deployments. As a cloud platform as a service (PaaS), it simplified deploying, managing, and scaling web applications, introducing developers to more efficient deployment processes compared to traditional server-based setups.

Firebase, initially a backend-as-a-service (BaaS), also contributed significantly. It provided developers with essential tools for building web and mobile applications, especially useful for real-time applications and database management.

In essence, while traditional web deployments may not offer the scalability and flexibility of newer technologies, their legacy lives on. Tools like cPanel and Heroku continue to be pertinent for certain deployment scenarios, balancing reliability and user-friendliness with technological advancements.

However, in the emerging JS-first world of the web, with new buzzwords like Edge, Serverless, CI/CD pipelines and what not, there’s another new breed of deployment foundations coming up. These traditional web deployments laid the groundwork for today's advanced web technologies. Don’t write them off yet though – their continued use underscores the balance that developers and businesses often seek between cutting-edge technology and proven, stable solutions.

## Shift to modern techniques

The evolution from traditional to modern web deployment techniques marks a significant shift in how devs approached building and managing websites and apps, primarily driven by the need for more scalable, efficient, and flexible development processes, so let’s look into a few of those unlocked achievements.

### Advantages of Modern Deployment Techniques

#### Scalability and Performance

Modern techniques prioritize scalability, allowing web apps to handle increased traffic and data loads smoothly (no one wants a Black Friday 529 or 404 anymore). This scalability is often achieved through cloud-based solutions and serverless architectures, which dynamically allocate resources as needed. It’s no surprise that [AWS, GCP, and Azure alone account for over 65% of the cloud market](https://www.megaport.com/blog/aws-azure-google-cloud-the-big-three-compared/) given the near-infinite real-time scalability 🤯

#### CI/CD

The adoption of CI/CD pipelines has also added big gains into the development process. Changes to codebases are automatically tested and deployed, significantly reducing the time and effort required for updates and ensuring that applications are always in a deployable state.

#### Increased DX and Productivity

Modern deployment tools streamline various processes, from code integration to server management. This automation reduces the manual workload on developers, allowing them to focus more on development rather than operational challenges, or spending the whole day waiting for branches to merge.

#### Improved Reliability and Uptime

Advanced deployment strategies come with enhanced reliability. Techniques like blue-green deployments and canary releases ensure that new versions are rolled out smoothly without disrupting the user experience. And even if it does, rollbacks to previous versions are much easier than they used to be when having to manually revive backups from a database.

### Features unlocked by Modern Deployment

#### Containerization

Technologies like Docker and BuildKit have popularized containerization, allowing applications to be packaged with their dependencies, ensuring consistency across different environments and simplifying deployment processes, rather than the entire app being a mammoth of a code-maze.

#### Microservice Architectures

Modern deployments often leverage microservices, breaking down applications into smaller, independently deployable services and APIs. This approach enhances flexibility and facilitates quicker updates. The approach has gotten so reliable, that entire suites are being replaced by Stacks (think a DXP going from being an all-in-one solution to a composable architecture consisting of individual APIs for a CMS, cart, PIM, CRM, etc.)

#### Serverless and Edge

The cool buzz from the late 2010s, serverless architectures abstract server management and infrastructure decisions away from devs. This means developers can focus on code, while the platform handles scaling, deployment, and infrastructure management. This, in many ways, gave rise to Edge Computing - where modern deployments deliver content closer to the user, reducing latency and improving content delivery speeds.

#### Automated Testing and Monitoring

Modern tools integrate automated testing and monitoring, ensuring that any deployment issues are quickly identified and addressed, maintaining the overall health of the application.

The shift to modern deployment techniques has fundamentally changed the landscape of web development. It offers a more agile, efficient, and scalable approach, focusing on the exponentially increasing demands of modern web apps and their end users. So let’s take a quick look at this, and look at some of the new solutions enabling this change.

## The rise of Serverless Architectures and Frontend Clouds

This move serverless architecture and frontend clouds has significantly influenced the web development landscape. This approach shifts much of the infrastructure management burden away from developers, allowing them to focus on code, while the backend dynamically adjusts to the application's requirements. But who’re the frontrunners on this? At Dato we’ve got all our starters ready to deploy with Vercel and Netlify, along with plugins and docs for other platforms too, so let’s take a closer look at some of the ones you should have your eyes on.

### Vercel

Specializing in frontend frameworks, especially Next.js, Vercel offers a developer-friendly environment with features like edge functions and optimal performance. Its automatic scaling and ease of use for deployment make it a top choice for developers looking to quickly launch and update their web applications. They also ship INCREDIBLY fast, and have one of the most active communities out there making NextJS + Vercel an obscenely common choice and for good reason.

### Netlify

A pioneer in Jamstack development (they basically coined the term), Netlify provides a robust platform for building, testing, and deploying web projects. Its strengths lie in continuous deployment from Git across a global application delivery network, enhanced with features like form handling, serverless functions, and large-scale automation.

### GitHub Pages

Tailored for hosting simpler websites and documentation, GitHub Pages is seamlessly integrated with GitHub, offering a straightforward solution for deploying static content directly from repositories. It’s particularly suited for developers already using GitHub for their repos and collaboration.

### Cloudflare Pages

With a focus on performance, Cloudflare Pages is optimized for serving static content across its vast global network (considering they’re arguably the largest CDN out there). It offers excellent integration with existing Cloudflare features, enhancing the security and speed of web applications.

### Others

While those are sometimes considered the big 4, there’s other mammoths out there from AWS, GCP, and Microsoft too that we can’t ignore, especially for large-scale and complex apps.

#### **AWS Amplify**

As part of Amazon’s cloud offerings, AWS Amplify stands out for its comprehensive approach to building full-stack applications, providing both frontend and backend solutions, including authentication, data storage, and an API gateway for serverless computing.

#### **Firebase**

Google's Firebase offers a wide range of tools for web and mobile development. Its hosting services are known for delivering static and dynamic content with high performance, and it’s often used alongside Google Cloud functions for an extended serverless architecture.

#### **Azure Static Web Apps**

This service from Microsoft integrates with Azure Functions, offering an environment for modern web app development. It focuses on first-class support for a variety of frontend frameworks and automatic deployment from GitHub repositories.

Are there more? Yes. Depending on your use case you’d see names like Google App Engine, Digital Ocean Droplets, Surge, Render, and more. Depending on your project, there’s definitely some due diligence needed to make sure you’re deploying on the best possible platform for you.

It’s worth noting that the move towards serverless architecture and frontend clouds is more than just a trend; it's a pretty big shift that offers unprecedented levels of efficiency and flexibility. As these technologies continue to evolve, they're set to define the future growth of web development and continue giving birth to a whole new ecosystem of really innovative and cool dev tooling!

### Side Notes

Although not directly related to the specifics of deployments in the general scope of this page, it's important to keep an eye on supplementary tooling choices and architectural decisions that highly impact your deployments and the end UX.

(This will be a rolling list of topics and concerns that come up from our community, so we'll try to keep it helpful and specific, and less vague 😬)

#### CDN & Caching

While most of the Academy is relatively vendor agnostic, we're going to be a bit biased here and cover CDNs from the perspective of how we do things at DatoCMS.

Your media assets like images and videos are already heavily cached through our partnerships with commercial CDNs. They should be fast and reliable around the world, and it's safe to directly serve these to your visitors. (Do check out our docs for how assets are handled, specifically for the way images are handled via imgix, and videos via Mux).

It isn't necessary to add your own CDN on top of ours unless you have special needs or can negotiate better pricing with another CDN vendor on your own. In general we already try to buy in bulk from our upstream providers and pass the savings onto you.  
  
We also cache your GraphQL API calls to our Content Delivery API whenever possible, but very long or complex queries may not be cacheable (you can see details in a query's response headers). In general we recommend statically building your frontend, pre-fetching data from our API at build time and only serving your visitors the built pages. This way you pay for fewer API calls, and your visitors get better performance since they don't have to wait for a round-trip from your servers to ours. If you have more real-time content not suitable for static builds, that might require a different setup, which our support would be glad to discuss with you on a case-by-case basis.  
  
Other DatoCMS systems, like our admin/editor UI or the Content Management API, go through different routes. These aren't as heavily cached, but they should not be user-facing anyway. They are generally used by your editors and developers, who have different needs than your visitors. If you have specific questions about these systems, we'd also be happy to discuss them on a case-by-case basis.

---

# Headless CMS FAQs

Source [academy]: https://www.datocms.com/academy/headless-cms/headless-cms-faqs.md

Learn the concepts, applications, and criteria that go into making the move towards headless content management

## **What is a Headless CMS?!**

A Headless CMS is a content management system that stores and manages content without a built-in presentation layer. It provides backend content management capabilities while enabling front-end delivery through APIs to any digital platform, offering greater design flexibility and multi-channel content distribution.

## **What are the benefits of a Headless CMS for developers?**

A Headless CMS accelerates content management and delivery across multiple platforms, with its architecture promoting rapid editing, scalable content management, and enhanced security. It empowers developers with the flexibility to use their preferred tools, allowing for creativity and efficiency in web development. This makes it a robust and adaptable solution for modern digital strategies, ensuring content reaches its audience effectively and securely.

## **What are the common use-cases for a Headless CMS?**

Headless CMSs shine in areas requiring flexibility, efficiency, and multi-platform content delivery. It's ideal for localized marketing websites, allowing rapid creation and updates of marketing content and microsites. In e-commerce, it enhances customer experience with fast-loading pages and personalized content. Its use extends to non-web content like mobile apps and digital signage, offering a unified approach to omnichannel marketing. For internal business applications, it's used in employee-facing intranets and vendor portals, streamlining content aggregation and distribution.

## **Headless CMS and SEO?**

Headless CMS could positively impact your SEO by facilitating faster website loading times and improved website performance, both of which are key ranking factors for search engines. By decoupling content from design, they allow for more streamlined and efficient content updates, keeping websites fresh and relevant. With their inherent flexibility, Headless CMS platforms support structured data and metadata management, further improving SEO. Overall, the adaptability and efficiency of Headless CMS make it an advantageous choice for businesses seeking to strengthen their SEO strategy and online visibility. There’s also the considerations on things like CDN, security, asset optimization, etc., that you can read more about on Headless CMS and SEO.

## **Explain like I’m five: How does a Headless CMS work?**

The LEGO analogy works best here. Imagine you have a big box of LEGO bricks (that's your content) and lots of different LEGO boards (these are the different websites and apps). A Headless CMS is where you store all your LEGO bricks. Whenever you want to build something, you can take the bricks from this cupboard and put them on any board you like. This means you can use the same bricks to make a racing car on one board, an airport on another, and a pirate ship on another, without changing the bricks themselves.

## **What is the best Headless CMS?**

It really depends on what you want to build and what other considerations you have when it comes to features, budgets, SLAs, API capabilities, etc. According to G2, some of the commonly mentioned top 10 best Headless CMS (alphabetically) are Contentful, Contentstack, DatoCMS, Hygraph, Kontent, Prismic, Sanity, Storyblok, Strapi, and Umbraco. Those would be a great starting point to research which Headless CMS is potentially the right one for you.

## **What are the top features of a Headless CMS?**

While all Headless CMS would have some variance to their feature set - here’s a list of common product-focused ones to look out for that all Headless CMS should cover:

-   Intuitive content modeling with a wide variety of field types
-   The ability to handle multiple projects and environments
    
-   Locales (ideally not just languages, but locale-based content; think Internationalization v. Localization)
-   Inbuilt asset management
    
-   Custom roles and permissions for users and APIs with Permanent Auth Tokens (PATs)
-   Rich editor experience with an intuitive editor and UI
    
-   Scheduled publishing, custom workflows, and content validation
-   A strong developer experience via well documented APIs, a CLI, SDKs for custom plugins, and a strong CDN network
    

Depending on your use-case, some features may be more important than others. For a good look into what [feature coverage and SLAs you can expect with DatoCMS](https://www.datocms.com/pricing.md), check out our plan comparison.

## **Is a Headless CMS a website builder?**

Yes and no – you can use a Headless CMS to build websites, but its capabilities go way beyond that. If you’re still unsure on whether you need a Headless CMS, [let’s talk](https://www.datocms.com/contact.md)!

---

# Introduction to Headless CMS

Source [academy]: https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md

Learn the concepts, applications, and criteria that go into making the move towards headless content management

## TLDR

-   There’s 3 main evolutions to content management – the traditional CMS (website builders) like WordPress, Decoupled CMS (slightly more technical content APIs) like Decoupled Joomla, and Headless CMS (backend delivering structured content via API).
-   A headless CMS is a backend-only CMS that lets users create and manage content in a structured format, that’s typically delivered via API in consumable formats like JSON, to any channel or platform.
    
-   Unlike a traditional CMS such as WordPress, a headless CMS does not dictate where or how content is shown.
-   A headless CMS enables developers to use their preferred tech stack or framework, including popular ones like React, Angular, and Vue, rather than being restricted to what the CMS requires (i.e. PHP for WordPress)
    
-   Depending on your scope and use case, Headless CMS offer several other benefits over traditional ones, such as security, scalability, omnichannel delivery, and performance.
-   A Headless CMS approach may not be right for you unless you have some technical resources at hand.
    

## What is a CMS?

Before understanding Headless CMS, it’s important to learn about the classical approach to content management via traditional CMSs. A Content Management System (commonly just called a CMS) is a software used to create, edit, manage, and publish organized content. Most commonly, CMSs are used for web content management and enterprise content management, for use cases like eCommerce, websites, blogs, and knowledge bases.

(Image content)

Web Content Management (WCM) allows content editors to publish websites without necessarily needing knowledge on HTML, web frameworks, or other technologies. WCM systems often include tools for creating and managing digital content such as text, images, and multimedia elements. Common WCM platforms include tools like Wordpress, Wix, and Squarespace – where the CMS handles all the heavy lifting on the database, backend, and code, giving editors a simple frontend editor to create and publish websites.

Enterprise Content Management (ECM) is used by companies to organize and store their corporate documents and other content. ECM systems often include features for document management, digital asset management, record retention, and workflow automation. Use-cases typically extend beyond websites into examples like intranets, knowledge bases, and portals, where features like security and permissions play a crucial role.

Regardless of whether you’re using a generic WCM or ECM, they all share some common features to make the experience user friendly.

-   User-Friendly Editor: Most CMS platforms offer a WYSIWYG (What You See Is What You Get) editor, allowing users to edit content without needing to write code.
-   Content Organization: CMSs provide ways to categorize and tag content, making it easier to manage, search, and use.
    
-   Roles and Permissions: Different levels of access can be set for users, allowing control over who can publish, edit, or view certain content.
-   Template-Based Design: CMSs use templates for a consistent look across all pages and content types.
    
-   SEO-Friendly Tools: Many CMSs offer tools to help optimize content for search engines, allowing users to set metadata and get some analysis on how SERPs would render their content.
-   Localization: Most CMSs offer in-built localization to create multilingual websites.
    
-   Extensibility: Through plugins or extensions for things like billing, shipping, etc., a CMS can be customized and extended to add new features or integrate with other systems.
    

Some popular examples of CMS include Wordpress, Drupal, Joomla, Shopify, and Magento – systems that have gained popularity for their ease of use for non-technical users, and extensibility options that allow them to integrate with and communicate with a wide variety of APIs and other services.

## From Legacy to Decoupled to Headless CMS

Eventually, classical CMSs like Wordpress started to show limitations for a variety of reasons. JS web dev got popular and people wanted really custom frontends. [Developers didn’t want to be restricted to PHP anymore and wanted to choose their preferred frontend and backend frameworks](https://www.datocms.com/blog/wordpress-vs-datocms.md). Editors wanted a bonkers level of customization. Teams needed to distribute content to websites, mobile apps, and dishwashers. You name it. Classical CMS couldn’t keep up beyond a single-platform strategy, and hacking it to make it work on more often wasn’t worth it. Enter Decoupled Content Management Systems (CMS).

Decoupled CMSs offered a separation of the front-end and back-end, giving developers some flexibility in working with a wider range of frameworks. Content was delivered via API, enabling integrations with various frontend systems and platforms, without the limitations of using pre-defined technologies that were specific to the CMS. Having more control over the content delivery also allowed for better security as the backend wasn’t exposed to the frontend, and finally unlocked the ability for teams to distribute content to multiple platforms and channels without needing a specific CMS for each entity.

This is all starting to sound suspiciously like a Headless CMS (we’ll get into that), but there’s one critical difference to establish before diving into that. While both [Headless and Decoupled CMSs provide more flexibility](https://www.datocms.com/blog/what-is-a-headless-cms.md) and better content management capabilities than traditional CMS, the difference lies in how they handle the front-end: Headless CMS provides no front-end tools, offering total freedom and responsibility for presentation, whereas Decoupled CMS might offer some level of front-end functionality or coupling. Decoupled, or Hybrid CMS, can be seen as a transition step in complexity between Traditional CMS and Headless CMS – Decoupled CMSs may have a steeper learning curve than traditional CMSs, but less so than headless CMSs, as it still provides some front-end tools.

In summary, while legacy CMS offer simplicity and an integrated approach, decoupled CMSs provide flexibility, improved performance, and better security, catering to modern web development needs. However, they may require more technical expertise to set up and manage, especially on the front-end development side.

## What is a Headless CMS?

A headless CMS is a content management system where the content repository is completely separated from the presentation layer, or frontend. A headless CMS allows you to manage content in one place and be able to deploy that content on any digital channel you choose.

Think of it as just a “content database” where editors add, edit, and manage content without any overlaps with how it eventually “looks” on the frontend, as that content would be rendered via API. Similar to databases, each content record (like a landing page, a banner, or a post) are just rows of data, available for developers to query onto their frontend(s).

It is essentially a [backend-only CMS](https://www.datocms.com/blog/what-is-a-headless-cms.md), as the term "headless" arises from that separation, where the backend, or the "body," is decoupled from the frontend or the "head."

(Image content)

Since the frontend and backend are split, the content repository of a headless CMS makes content accessible via an API to any frontend, such as a website, mobile app, or other "head." Most often, this content is delivered in a raw structured format, such as HTML or JSON, and isn't really meant for human consumption until rendered on the end device.

This API-driven approach offers many advantages over traditional CMSs:

-   By removing the head there are theoretically no restrictions on how or where content can be delivered – whether to visual platforms like websites or mobile apps, or to non-visual destinations like voice assistants or smart devices.
-   Marketing and editorial teams can create content within the editor interface of a headless CMS. This is similar to how they would with traditional CMSs like WordPress. Meanwhile, the engineering team can define how and where this content is delivered by creating a frontend on the channel where content will be rendered.
    
-   Developers are also free of a traditional CMS's templating and framework restrictions. For context, using WordPress typically means needing a WordPress or PHP expert in-house, since WordPress’s backend is in PHP. Headless CMS just offer a REST or GraphQL API, meaning frontend developers can own the process end-to-end without necessarily needing dedicated backend resources.
-   With a headless CMS, they can take advantage of frameworks they’re used to and create frontend experiences using [React](https://www.datocms.com/cms/react-cms.md), Angular, [Vue](https://www.datocms.com/cms/vue-js-cms.md), [Next.js](https://www.datocms.com/cms/nextjs-cms.md), or any modern technology as they see fit.
    
-   A headless CMS offers greater flexibility than a traditional CMS, where "content" is restricted to a landing page or a blog post. There are virtually no limitations as to what can be considered content, including anything from blog posts and landing pages to banners, alerts, flight inventory, and news feeds.
-   Similarly, there are no restrictions on platforms where this content can be delivered, which can extend from websites and mobile apps to smart tablets and watches or even IoT-connected kitchen appliances like dishwashers and fridges.
    

So, going back to the whole “but Decoupled CMS sounds a lot like Headless CMS”, it should now be easier to look at some of the differences between the two.

| Criteria | Decoupled CMS | Headless CMS |
| --- | --- | --- |
| Front-end and Presentation Layer | It typically provides a front-end delivery layer, which means it can render pages and manage how content is displayed. This front-end layer is separate from the content management back-end but is still a part of the CMS. | It has no front-end layer or presentation capabilities. It's solely focused on the back-end management of content, which is delivered via APIs to any front-end system, such as a website, mobile app, TV, etc. |
| APIs and Content Delivery | Uses APIs for content delivery, but this is often in addition to the integrated front-end system. These APIs allow content to be used elsewhere but are not the only method of content delivery. | Relies entirely on APIs (REST, GraphQL, etc.) to deliver content. This API-first approach ensures flexibility in how and where content is displayed. |
| Flexibility and Control | Offers more flexibility than traditional CMS systems but may provide some structure or constraints for the front-end. | Offers complete freedom for developers to build the front-end using any technology or framework they choose, without any constraints imposed by the CMS. |
| Use-cases and Users | For users who want more flexibility than a traditional CMS but appreciate some level of structure or guidance for front-end development. | For projects that require absolute customization on the front-end, complete freedom in selecting backend technologies and APIs, and need to distribute content across various channels and devices. |
| Complexity of implementation | May have a steeper learning curve than traditional CMSs, but less so than headless CMSs, as it still provides some front-end tools. | Requires a strong understanding of both back-end and front-end development, as it necessitates building the front-end from scratch. |
| Common examples | Decoupled Drupal, Decoupled WordPress, Decoupled Joomla, Directus | DatoCMS, Contentful, Strapi, Sanity |

## Why use a Headless CMS?

Ever since modern web development (we’re talking React/JS-first websites) picked up and performance, customization, and creativity became more important to web-based projects, Headless CMS have picked up in popularity. While the concept and architecture has been around for a while, a simple look into Google Trends shows that the traction towards Headless CMS has grown quite a bit in that period.

(Image content)

What's the reason behind this growing interest?

"Headless" has become the popular approach to handling content due to the increasing diversity of platforms that need content, the improved developer experience it offers, and overall faster app load times, among other benefits.

Let’s take a look at some of the more commonly quoted reasons.

### **Future-Proof Content**

We’ve already highlighted that content today isn’t just for websites, but for a massive range of visual and non-visual destinations. Since a Headless CMS separates your content from the presentation layer, no matter how tech trends change, your content remains intact, ready to be pushed to any new platform or device. You don't need to worry that a headless CMS won't be able to deliver content to another digital channel that pops up years from now.

### **Design and Development Flexibility**

Ever felt boxed in by traditional CMS limitations, like a specific template or a framework? Since Headless CMS don’t have any opinion on how and where the content is delivered, you’re free to design and develop without constraints, using the tools and frameworks you love. This freedom lets you craft unique experiences for your users, rather than fitting your vision into predefined templates.

### **Performance and Security**

Without the front-end part, a Headless CMS is less susceptible to common security threats that plague traditional CMSs. Most Headless CMS also work with the concept of API permissions and tokens, where you’re in control of defining which users, roles, and even APIs can access what content.

Also, since content delivery is separate from content management, your site's performance can skyrocket (think potential 100s across the board on your next Lighthouse test, SEO people). Faster load times, happier users.

### **Ease of Integration**

Whether it’s AI, eCommerce, memes, personalization, or any other tool, integrating with a Headless CMS is a breeze. If they’ve got an API, they can be integrated. This ease of integration opens up a world of possibilities for enhancing your digital offering without getting entangled in complex middleware building just to make 2 or more services work together.

### **Content Scalability and Omnichannel Delivery**

A Headless CMS is like a content hub. Manage your content in one place and distribute it across multiple channels – websites, apps, IoT devices, you name it. This scalability ensures your content strategy is cohesive and consistent, no matter where your audience interacts with your content. As a bonus, when working with well-structured and modular content, it’s worth maintaining a “content architecture” to understand what pieces of content are reusable across destinations to avoid unnecessary duplication and manual work, and just have one piece of content sourced to multiple devices and channels.

If all that sounds great, it’s also important to highlight that going Headless may still not be for everyone. If you're looking to build a robust, scalable, and flexible digital presence, a Headless CMS is definitely worth considering. If you’re just building a simple blog, or a one-pager static site, or don’t have any engineering resources handy, there’s certainly simpler options to consider.

There’s also several caveats when going Headless, and times where it may not be the right decision – we’ll cover that further on in the challenges of going headless.

---

# How a Headless CMS works

Source [academy]: https://www.datocms.com/academy/headless-cms/how-a-headless-cms-works.md

Learn the concepts, applications, and criteria that go into making the move towards headless content management

## TLDR

-   The overall CMS architecture significantly differs between traditional and Headless CMS with the separation of the 'body' (the content repository) from the 'head' (the presentation layer).
-   Content modelling is perhaps among the most important concepts to get familiar with, as it sets the foundation for how a CMS implementation impacts the overall project(s).
    
-   Content and assets are often categorized into different types and records, like articles, products, or pages, each with their own set of attributes and metadata.
-   Typically, there’s a few key types and flavors of APIs to keep in mind when choosing or implementing a Headless CMS, including but not limited to a Content Delivery API, Images API, and Real-time Updates API.
    

Let’s look at some of the core components and approaches that make headless content management stand out from traditional approaches – along with some concepts that highlight the difference.

## Architecture and Content Overview

With Headless Content Management, the architecture significantly diverges from traditional CMS approaches, focusing on how content is managed and delivered. The core principle is the separation of the 'body' (the content repository) from the 'head' (the presentation layer). This decoupling is pivotal in understanding how content is handled in a headless CMS.

At its core, the architecture of a headless CMS is API-centric. Think of it as a database, housing rows of content entries. The content is stored in a raw, standardized format not typically meant for human consumption, often JSON, and delivered (commonly) via REST or GraphQL APIs. This format is universally accessible and facilitates easy distribution across various platforms – be it web, mobile, or IoT. The absence of a predefined presentation layer grants developers the freedom to use any framework or technology to display the content.

(Image content)

In a headless CMS, the content architecture is designed to be agile and modular. Content is typically organized into reusable blocks or components (content modeling), allowing for efficient management and scalability. This modular approach not only streamlines content deployment across different channels but also enables a more structured and maintained content repository.

## Content Modeling

[Content Modeling is perhaps one of the most important concepts](https://www.datocms.com/docs/content-modelling.md) to get familiar with when moving to Headless CMS. It defines how content is structured and interconnected.

Unlike traditional CMS where content structure is often designed with a specific presentation in mind, in a headless CMS, content modeling is done in a more abstract, presentation-agnostic way. This approach offers significant flexibility and reusability of content across various platforms and devices.

At its core, content modeling involves defining content types and their relationships. A content type, such as a blog post, product page, or user profile, is a blueprint for a specific kind of content, detailing the fields and the nature of the data each field contains. These fields can include text, dates, media, and even links to other content types, enabling the creation of rich, interconnected content structures without the need to recreate blocks whenever other pages or content types require the same components.

A simple way to visualize this is remembering the Lego analogy. Imagine each piece of your content as a Lego block. Content Modeling is akin to designing these unique Lego pieces - each with its distinct shape, size, and purpose. Just as Lego blocks can be assembled in countless ways to create everything from simple structures to complex models, content elements in a headless CMS are designed to be modular and reusable. This modular approach allows you to build diverse digital experiences, from web pages to mobile apps, by rearranging and reusing these content blocks. Just like a Lego set, where the same pieces can construct a castle, an airport, or a beach, in content modeling, the same content pieces can be configured to create an array of digital experiences.

(Image content)

A well-designed content model enhances content discoverability and usability. By creating clear, logical structures, content can be more easily found, managed, and utilized, leading to a more efficient content operation process. In essence, content modeling is the foundation upon which effective and dynamic content strategies are built.

## Content and Asset organization

Content and asset organization is the next aspect that ensures efficiency and ease of content management, as well as seamless content delivery. Headless CMS treats content and assets – like text, images, and videos – as separate entities from the presentation layer, allowing for their independent management and reuse across multiple platforms.

(Image content)

Content is often categorized into different types and records, like articles, products, or pages, each with their own set of attributes and metadata. This categorization is complemented by tagging and metadata, which play a crucial role in making content searchable and manageable. For instance, tagging articles with keywords like 'technology' or 'healthcare' allows for easy retrieval and grouping of related content.

(Image content)

Assets, such as images and videos, are managed through [digital asset management (DAM)](https://www.datocms.com/blog/discover-the-all-new-datocms-asset-management.md) systems, which are either integrated into the Headless CMS or connected as an external service. In the case of DatoCMS, asset management for images and videos is built-in, using best of breed APIs and capabilities from other providers to ensure assets are well optimized and correctly served. These systems enable the centralized storage, organization, and retrieval of digital assets, ensuring they are easily accessible and reusable.

In essence, the organization of content and assets in a Headless CMS is about creating a structured, yet flexible, repository that supports efficient management and maximizes the potential for content to be used in diverse and innovative ways.

## Content APIs

Saving the best for last, let’s actually dive into the whole concept of Content APIs, and why this approach is so important to understand when comparing traditional CMS to headless ones.

Content APIs are everything in the functionality of a Headless CMS, acting as the conduits through which content and assets are delivered to various front-end platforms. These APIs ensure that content stored in the Headless CMS can be accessed and displayed on websites, mobile apps, IoT devices, and more, irrespective of the technology used on the front end. In some cases, when using integrations and plugins, they also help programmatically source content from other APIs to enrich the content in the CMS, allowing teams to build truly remarkable digital experiences from endless sources.

Typically, there’s a few key types and flavors of APIs to keep in mind when choosing or implementing a Headless CMS.

### **Content Delivery API**

The workhorse behind the delivery of content to various platforms. It's responsible for serving content requests from front-end applications. The [Content Delivery API](https://www.datocms.com/docs/content-delivery-api.md) pulls the required content from the Headless CMS and delivers it in a format that's consumable by the requesting application, often JSON. This process is crucial for ensuring that the right content is displayed in the right context, maintaining consistency across different channels.

### **Content Management API**

While the Delivery API is about getting content out, the [Content Management API](https://www.datocms.com/docs/content-management-api.md) focuses on content input and organization within the CMS. This API allows for programmatic interactions with the CMS, enabling content creators and editors to add, modify, and organize content, often through a UI, CLI, or via API with write access. This capability is vital for maintaining an up-to-date and dynamic content repository, which in turn ensures that the Delivery API can serve relevant and current content.

### **Real-time Updates API**

As a cherry on the cake, real-time updates are invaluable to use-cases like live news, stock tickers, and sporting events to name a few. APIs that support [real-time updates, akin to GraphQL Subscriptions](https://www.datocms.com/docs/real-time-updates-api.md), allow applications to automatically update the displayed content as soon as a change occurs in the CMS without needing a new build to be pushed. By subscribing to specific content updates, front-end applications can receive instant notifications and refresh the content, ensuring that the end-user always has access to the latest information.

### **GraphQL APIs**

These APIs have gained prominence for their efficiency and flexibility. Unlike traditional APIs, [GraphQL](https://www.datocms.com/blog/graphql-and-datocms.md) allows clients to query exactly what they need, nothing more or less. This means that a mobile app and a web platform could use the same API but fetch different data as per their requirements, reducing the load and improving performance.

### **REST APIs**

Representational State Transfer (REST) APIs are widely used for their simplicity and statelessness. They work on a standard request-response model, making them easy to understand and implement. REST APIs are particularly effective for basic content retrieval and are supported by a wide range of platforms.

### **Asset APIs**

Specifically designed for managing digital assets like images and videos, [Asset APIs](https://www.datocms.com/features/images-api.md) are crucial in a Headless CMS. They allow for efficient handling, retrieval, and optimization of media files, which is vital in delivering rich media content across diverse platforms.

However, in all cases, caching plays a super important role in performance optimization. Global caching mechanisms ensure that content and assets are stored closer to the end-user (think on the edge, in CDN speak), reducing load times and server bandwidth usage. This is particularly crucial for content that doesn't change frequently, enabling quick and efficient content delivery across the globe. While the API variety and selection offered by a CMS is a core consideration, a CDN and a caching mechanism is equally important to consider.

---

# Headless CMS and SEO

Source [academy]: https://www.datocms.com/academy/headless-cms/headless-cms-and-seo.md

Learn the concepts, applications, and criteria that go into making the move towards headless content management

## TLDR

-   The foundation for succeeding with a [Headless CMS and SEO](https://www.datocms.com/docs/content-modelling/seo-fields.md) is ensuring the technical implementation is done correctly - from getting the basics right by ensuring HTTPS everywhere, to correctly opting for a balanced approach to dynamic rendering - there's several considerations for setting up a Headless CMS.
-   Headless CMS and SEO can have several limitations, and isn't a WYSIWYG + Plugin combo as you might be used to with traditional CMS.
    
-   Original content aimed at people, proper keywords, interlinked content, and backlinks from reputable sources are still of prime importance for strong SEO, regardless of the CMS.
-   Editorial best practices are as important as technical ones.
    

This is such an incredibly interesting and “it depends” topic, that there’s no blanket answer for whether or not a Headless CMS is better for SEO for you – it all comes down to the technical implementation, and following best practices from the development and editorial side.

## Technical Implementation of a Headless CMS for SEO

The technical implementation of a Headless Content Management System (CMS) for SEO poses unique challenges and opportunities. Unlike traditional CMSs, where content and presentation are intertwined, a headless CMS separates content management from content delivery, relying on APIs for the latter. This architecture impacts several technical aspects crucial for SEO. Let’s take a quick look at several of the factors and best practices that go into setting up your CMS for being SEO ready.

##### **Use HTTPS**

(Image content)

A bit of a no-brainer to start with, really. Securing your site with HTTPS is a must for SEO. HTTPS not only protects the integrity and confidentiality of your users' data but is also a ranking signal for search engines. When using a headless CMS, ensure that your entire content delivery infrastructure supports HTTPS, including secure API endpoints.

##### **URL and slug structure**

In a headless CMS, since the content delivery is separate, you have complete control over URL structures – a critical factor in SEO. It’s essential to create clean, readable URLs that include target keywords and are logically organized. Implementing [SEO-friendly URL structures](https://www.datocms.com/docs/content-modelling/slug-permalinks.md) requires careful planning in the content delivery layer, ensuring that URLs follow a consistent, navigable, and intuitive pattern.

##### **Semantic HTML**

Using semantic HTML tags is essential for SEO as it helps search engines understand the structure and content of your pages. In a headless CMS, you have the freedom to structure your content as you see fit. However, on the repo itself, use semantic elements like `<header>`, `<footer>`, `<article>`, and `<section>` to define different parts of your content. Proper use of `<h1>` through `<h6>` tags for headings and subheadings also helps in structuring content hierarchically, making it more accessible and understandable to search engines.

*Interestingly, as of 2024, it's a hot topic again as to whether or not the HTML structure actually matters. Here's an insight into Google "confirming" that* [*HTML Structure Doesn't Matter Much For Ranking*](https://www.seroundtable.com/google-html-structure-seo-rankings-36789.html)*.*

##### **Schema Markup**

(Image content)

Google recommends using JSON-LD for structured data - this is vital for enhancing search engine understanding of the page content. In a headless setup, you need to integrate schema markup within your repo. This involves tagging your content with relevant schema.org types, which can be managed through the CMS and delivered via APIs to the front end, where it's rendered with the correct schema. Check out the recommendations from Google's side, to see whether your content or content types have any applicable best practices. The example shown is for rich rendering of a `recipe`.

##### **Site Speed Optimization**

(Image content)

We all love a row of 100s on the Lighthouse! Headless CMS architectures can significantly enhance site speed when implemented correctly – a key Google ranking factor. The lightweight nature of headless CMS, devoid of front-end bloat, means faster content delivery. Leveraging technologies like CDN (Content Delivery Networks) for asset delivery, optimizing images and videos, and ensuring efficient API calls all contribute to improved load times.

##### **CDNs**

Speaking of performance and CDNs, use one! Using a [CDN can drastically improve site speed and user experience](https://www.datocms.com/features/worldwide-cdn.md). A CDN stores a cached version of your content in multiple locations to reduce latency when accessing your site. In a headless CMS setup, static assets like images, CSS, and JavaScript files should be served through a CDN.

##### **Mobile Responsiveness**

Given the mobile-first indexing approach of search engines, your content must cater to mobile users effectively. The front-end technology stack needs to be chosen and optimized with a mobile-first approach. Responsive design principles should be applied to ensure content renders well on all devices, which can be managed through separate APIs for different device types. While the CMS itself has no impact on your mobile friendliness, the API performance for content and assets can help a lot here when it comes to querying content on your side.

##### **Dynamic Rendering**

For content-heavy websites, dynamic rendering can be a solution to SEO challenges. It involves serving a version of the content that’s easily crawlable for search engines. Implementing dynamic rendering in a headless CMS context involves setting up server-side rendering or using pre-rendering services for complex JavaScript applications.

##### **Server-side rendering**

For JavaScript-heavy sites, server-side rendering is vital for SEO. SSR renders the JavaScript on the server, turning it into static HTML which is then sent to the browser. This process ensures that search engine crawlers can index your content effectively, as they sometimes struggle with heavy client-side JavaScript.

##### **Static Site Generation**

Related to SSR and load times, depending on your use-case, many aspects if not all can be leveraging SSG for your web-first approach. Generating static pages from a headless CMS can improve site performance and SEO. Static sites load faster as they are simple HTML files without server-side processing. Modern static site generators like Jekyll, Hugo, and 11ty can pull content from a headless CMS and pre-render it into static files, combining the benefits of a [CMS with the performance of static websites](https://www.datocms.com/docs.md).

##### **SEO-Friendly Content Delivery**

In a headless CMS, content is delivered via APIs, and how this content is rendered on the client side can impact SEO. Ensuring that the content, especially textual content, is fully crawlable and not reliant on JavaScript for rendering is crucial. The big G doesn’t like it when a lot of text is hiding behind JS rendering, so ensure that your entire content is robot friendly as much as human friendly. Implementing server-side rendering or hybrid rendering approaches can solve these issues.

##### **Image SEO**

(Image content)

Last but not least, it’s always good to remember that optimizing images is crucial for SEO - as much as we loathe going in to update alt\_texts and the like. This includes compressing images, using appropriate file formats, and providing alt tags for accessibility. In a headless CMS, you can handle image optimization through automated tools and ensure that the API delivers images in optimized formats and sizes for different devices.

## Benefits of a Headless CMS on SEO

Once the technical implementation is in place, editors normally have a breeze managing their content within a well-structured CMS environment. Ongoing content production and optimization have led to several emerging benefits of adopting a Headless approach – particularly with UX and editorial flexibility at the center of SEO gains.

##### **Enhanced UX**

A key benefit of a Headless CMS is the ability to deliver a superior user experience. With its front-end freedom, designers and developers can create fast, responsive, and visually appealing websites that delight. A better UX leads to lower bounce rates and higher time on site, metrics that search engines consider when ranking sites (bounce rates are arguable for 2024, but we all love a good time-on-site metric!). By prioritizing user satisfaction, a Headless CMS indirectly boosts SEO through improved user engagement.

##### **Flexibility in Content Delivery**

Headless CMS allows for content to be delivered and optimized for any platform. This multi-platform compatibility ensures that content is accessible and optimized for users across all devices, a crucial factor in mobile-first indexing strategies of modern search engines.

##### **Streamlined Content Management**

The backend flexibility of a Headless CMS empowers content creators with better workflows and easier content management processes. This advantage allows for quicker content updates, more efficient processes, and the agility to respond to trending topics or content opportunities. Timely and relevant content is key to keeping a website fresh and engaging, which is favored by search engines.

##### **Improved Site Speed and Performance**

The decoupled nature of a Headless CMS often results in faster website speeds, as the content delivery is not bogged down by the weight of the front-end presentation layer. Faster site speeds lead to better user experiences and are a direct ranking factor for search engines.

##### **Technical Opinions**

Headless CMSs indirectly force the implementation of various technical SEO best practices such as structured data, semantic HTML, and efficient content delivery via APIs, since developers have to implement best practices from the get-go. These technical underpinnings are essential for improved crawlability and indexability by search engines. The ease of integrating these features means that websites can maintain a technical edge.

By enhancing user experience, offering flexible content delivery, streamlining content management, and facilitating technical SEO best practices, a Headless CMS can bring immense gains to the overall SEO for a team provided that the implementation and best practices are followed.

## Challenges when using a Headless CMS for SEO

It’s also worth noting that going Headless isn’t a magic bullet for great SEO. There’s many challenges, and at times, given the team/use-case, it may even not be recommended to go Headless! Let’s take a look at some common challenges we’ve seen pop up.

##### **You need development resources**

This can’t be underestimated enough. Whether working with an in-house team or an agency, going headless requires sufficient technical expertise. In a headless environment, even simple SEO tasks may require developer intervention. Unlike traditional CMS where marketers can directly make changes, headless systems might necessitate coding for updates like meta tags, structured data, or even URL changes. This dependency can slow down the ability to respond quickly to SEO needs or market changes.

##### **Complexity of Implementation**

Shifting to a headless architecture often requires a steep learning curve, especially for teams accustomed to traditional CMS platforms. This complexity extends to setting up SEO-friendly features which, in a headless setup, might require custom development. Ensuring that all SEO best practices are implemented effectively can be more resource-intensive compared to a conventional CMS considering your situation.

##### **JavaScript Rendering Issues**

Websites built on JavaScript frameworks can face challenges with crawlers. If not properly managed through techniques like server-side rendering or dynamic rendering, JavaScript-heavy sites can suffer from delayed or incomplete indexing.

##### **Ensuring Consistency**

One of the advantages of a headless CMS is also its challenge. Ensuring that content is consistently optimized across all platforms requires careful planning and execution. Missteps in content consistency and optimization can lead to a fragmented user experience.

##### **Lack of Built-in SEO Tools**

Headless CMS don’t have a one-click solution using plugins like Yoast and AllInOne SEO simply because they don’t “know” your website or end platform. All this needs to be configured. Traditional CMSs often come with built-in SEO tools and plugins that simplify optimization. In a headless CMS, these tools may not be readily available, or may require some custom work to connect your website analytics to your CMS.

In a headless CMS, adapting to new SEO trends might require more than just content updates; it may involve changes to the API, front-end, or even the infrastructure, necessitating ongoing investment in development resources. It’s worth keeping all these in mind when the time comes to choose a Headless CMS.

---

# Use Cases for Headless CMS

Source [academy]: https://www.datocms.com/academy/headless-cms/use-cases-for-headless-cms.md

Learn the concepts, applications, and criteria that go into making the move towards headless content management

While definitely not an exhaustive list (we’ve seen Headless CMSs used for everything from websites and online shops, to voice-triggered responses on virtual assistants and AI generated metadata for media), this section covers some of the supremely common use-cases we encounter from teams considering to switch to a Headless CMS.

## Modern Websites

Modern websites are more than just the corporate websites they used to be - they are dynamic, interactive platforms that come closer to being classified as “digital experiences”. Given the business impact, user expectations, and technology play a strong role in shaping modern websites, having a flexible and scalable web presence is crucial. For more advanced website use-cases, a Headless CMS can make a big difference.

### Headless CMS for Modern Websites

Headless CMS plays a pivotal role in modern web development. By decoupling the content management from the presentation layer, it offers flexibility in how content is delivered and presented, making it ideal for the ever-changing modern websites.

A Headless CMS is not restricted by front-end frameworks, making it a perfect fit for modern websites that prioritize speed, responsiveness, and cross-platform compatibility. It allows developers to use their preferred tools and technologies to create web experiences. Additionally, the API-driven nature of a Headless CMS ensures that content updates are swift and seamless, keeping websites fresh and engaging.

### Key Considerations for implementing a Headless CMS

#### Choosing the Right Framework

Align your choice of front-end technology with your website's goals and audience needs.

#### SEO Optimization

Ensure your chosen solution supports SEO best practices, as Headless CMSs require additional considerations for search engine visibility. We’ve covered this exhaustively in [another chapter for Headless CMS and SEO](https://www.datocms.com/academy/headless-cms/headless-cms-and-seo.md).

#### Content Strategy

Develop a content strategy that leverages the flexibility of a Headless CMS, focusing on dynamic and interactive content.

The adoption of a Headless CMS can make a big difference for modern websites. It offers the flexibility, scalability, and speed required to meet today’s user demands.

## eCommerce

Similar to websites, eCommerce platforms are rapidly evolving, demanding more flexibility and integration capabilities than ever before.

## Headless CMS for eCommerce

There’s often two schools of thought in modern eCommerce:

-   The all-in-one approach for simpler implementations, using a full-on eCommerce solution like a Shopify,
-   The composable Headless eCommerce stack for flexibility using best-of-breed APIs for carts, shipping, product inventory, etc.,
    

In either case, the CMS forms the core of the entire experience, often generating all the content for the website, app, or other commerce enabled platform, further enriched by product related content from other platforms.

A Headless CMS is crucial in the eCommerce ecosystem for its ability to provide content management that is independent of the front-end UI, and flexible enough to either consume, or collaborate with content coming and going from other APIs to really provide a seamless UX to the user. With the complexities of modern eCommerce that includes Headless Commerce, shipping APIs, cart APIs, and other composable APIs, a Headless CMS stands out for its versatility. It allows for the seamless integration of various commerce-related services, enabling businesses to create highly customized and scalable online shopping experiences. Furthermore, the API-first approach of a Headless CMS ensures that content delivery is fast and consistent across all channels, which is vital for maintaining a competitive edge in eCommerce where performance is arguably the most important factor.

### Key considerations with a Headless CMS for eCommerce

#### Diverse API Integrations and Extensibility

Strategically integrate various commerce-related APIs for a cohesive system that includes product management, shopping carts, and payment gateways.

#### Focus on User Experience

Design the front end to provide an engaging and intuitive shopping experience, leveraging the CMS's flexibility.

#### App-first approach

Notice we didn’t say Mobile-first. Though a LOT of eCommerce is mobile centric, the web experience is equally important, and most eCommerce platforms are modern web apps, rather than just a website. Ensure the eCommerce platform is optimized for all devices, and provide a seamless experience across mobile and web.

#### Secure and Robust

Given the sensitivity of customer data in eCommerce, especially when multiple APIs interact with content around payment info, delivery details, and CRM fields per user, prioritize secure API interactions and data protection.

## Localized content

Choosing a Headless CMS for Localization is a supremely common use-case. Whether brands are operating in 2-3 markets, or globally, the ability to create content for each segment can be a crucial consideration. For global brands, the ability to connect with a diverse audience is not just a nice-to-have, it's essential. This is where localized content steps in.

Localized content goes beyond mere translation (or internationalization); it involves adapting your content to meet the cultural, linguistic, and commercial needs of different regions.

Think `de_DE`, `de_AT`, and `de_CH`, to provide localized content that’s different to Germany, Austria, and Switzerland, rather than just giving everyone in DACH a `/de`.

However, managing this localized content can be a daunting task, often involving challenges like maintaining brand consistency while ensuring local relevance, handling multiple languages, and managing region-specific content or product variations.

A good localization-friendly Headless CMS plays a pivotal role in simplifying these challenges. It allows brands to manage a central repository of content that can be adapted and delivered to various regional platforms. And furthermore gives editors the ability to enrich that central content with specific content created for each locale. However, there’s a few things to keep in mind to get it right.

### Best practices for Headless CMS Localization

#### Structured Content Approach

Organize your content into modular blocks that can be easily mixed, matched, and reused across different regions. This structure supports localization by allowing for the adaptation of specific content pieces without the need to overhaul entire pages or templates. We can’t stress the importance of strong content modeling enough here – so much so that we’ve dedicated another [chapter on content modeling](https://www.datocms.com/academy/headless-cms/how-a-headless-cms-works.md#content-modeling) to walk you through it.

#### Consistent, but Flexible Framework

Develop a content framework that maintains brand voice and consistency across regions, yet is flexible enough to accommodate local nuances. This balance is crucial for global appeal with local resonance.

#### Efficient Workflows

Implement workflows in your Headless CMS that support collaboration among diverse teams. This involves clear roles and permissions for local teams to contribute and adapt content, ensuring it's culturally appropriate and relevant. Workflows go beyond just content creation though, and there're several aspects you need to consider.

#### Localization Features and Plugins

Integrate localization tools and services with your Headless CMS. These tools can assist in translation, adaptation, and managing various content versions, making the process more efficient and scalable. While most CMS should offer the ability to create localized content, some would go one step further and offer rich plugins that assist the entire process from end to end, and also provide for locale-based publishing to ensure that you don’t have to make changes globally when updating a specific market.

## Knowledge bases and portals

Let’s first clarify what we mean when we say Knowledge Bases and Portals – we’re covering use-cases of content-heavy platforms like Wikis, Intranets, API Documentation, Help Centers, Resource Hubs, etc., where a large amount of information is intended to be consumed by a specific type of reader – authenticated or public.

These are essential tools in disseminating information and resources to specific audiences, regardless of whether they’re internal repositories for employee training to customer-facing FAQs and technical guides.

### Why use a Headless CMS for Knowledge Bases?

A Headless CMS is particularly well-suited for managing these knowledge platforms due to its content-first approach and flexibility.

#### Centralized Content Repository

One of the key strengths of using a Headless CMS for knowledge bases and portals is its ability to serve as a centralized repository for content. This centralization is crucial when managing extensive databases of information that need to be consistently updated and disseminated across multiple channels.

#### Ease of Maintaining Content

Frequent updates are common in knowledge bases, whether it's new product information, updated policies, or updates to educational content. A Headless CMS streamlines this process, allowing for quick content updates without the need for backend changes in most cases, and the ability to use in-line entries referencing other content records ensures that information is consistent when changed.

#### Customized Content Delivery

Different types of knowledge bases require unique content modeling structures. For example, an internal Wiki may prioritize searchability and internal linking, while support portals might focus on FAQs and troubleshooting guides. A Headless CMS can cater to these varied needs by delivering content in a structured and efficient manner, tailored to the specific requirements of each type of portal.

#### Scalability and Extensibility

As organizations grow, so does their need for more comprehensive knowledge management systems. A Headless CMS can easily scale to accommodate this. Moreover, its ability to integrate with other tools like localization systems, analytics platforms, and AI-powered search APIs enhances the effectiveness of end platform.

#### Enhanced UX and Localization

Ultimately, the success of a knowledge base lies in how effectively users can search, navigate, access, and interact with the information. The flexibility in frontend development provided by a Headless CMS means that organizations can design user-centric interfaces that are intuitive and responsive, catering to the specific needs of their audience, all while the information is kept well structured in the back.

Whether it’s for an internal team looking for quick access to policies or a customer in need of a guide, a Headless CMS offers an adaptable, scalable, and efficient solution for managing knowledge bases and portals.

---

# Frameworks and Technologies

Source [academy]: https://www.datocms.com/academy/modern-web-development/frameworks-and-technologies.md

Understand some common use-cases, APls, and frameworks that go into building modern web experiences with Headless CMS

## What is the Jamstack and a Jamstack CMS?

Jamstack, coined by the folks over at [Netlify](https://jamstack.org/), represents a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup - the three foundational pillars being JavaScript, APIs, and Markup (JAM). This approach decouples the frontend from the backend, enabling faster, more secure, and scalable web solutions.

A Jamstack CMS, often just a headless CMS, fits into this architecture by providing content through APIs, ready to be consumed by JavaScript-driven frontend applications. Unlike traditional CMSs, a Jamstack CMS doesn’t concern itself with how content is displayed, focusing solely on content management and delivery.

### Why use a Jamstack CMS?

There’s a few reasons why Headless CMS and Jamstack approaches have surged in popularity over the past few years, whether or not “Jamstack” as a term is as prevalent as it was in the past.

#### Performance and Speed

In the Jamstack model, websites are prebuilt into static pages (traditionally\*, we’re not getting into hybrid dynamic etc., yet), reducing the time to the first byte and ensuring high performance (think about those juicy Lighthouse 100s). A Headless CMS complements this by efficiently managing and delivering content via APIs, which can be quickly integrated into SSGs (Static Site Generators).

#### Flexibility and Control

Jamstack allows developers to use their preferred tools and frameworks for the frontend, and a Headless CMS aligns perfectly with this principle. It provides the freedom to choose any frontend technology, as it purely delivers content via API without dictating the presentation layer.

#### Security

With Jamstack, most of the website is composed of static files, which inherently reduces security vulnerabilities. A Headless CMS enhances this by limiting direct database exposure and handling content operations through secure API calls via authorized tokens.

#### Scalability

Being static, Jamstack sites are inherently scalable. A Headless CMS further facilitates scalability by managing content in a way that easily adapts to increasing traffic and content demands, without the need for complex scaling of a traditional CMS’s backend. Think global cache around Edge POPs around the world, and how most requests can hit the cache given the sites are statically pre-built.

#### DX and Workflows

Developers benefit from a more streamlined and efficient workflow. They can focus on building the frontend, leveraging the Headless CMS for content management. This separation of concerns leads to faster development cycles and less overhead in maintaining and updating the website.

#### SEO and Performance

Headless CMSs allow developers to optimize content for SEO effectively. Since content delivery is decoupled, it’s easier to manage SEO aspects like metadata, structured data, and content optimization, which are critical for Jamstack sites.

So it’s quite natural to see why modern web development is JS-centric ([fun thread on Reddit from a few years ago](https://www.reddit.com/r/javascript/comments/pa73t6/askjs_what_is_going_on_with_all_websites_in_js/) on the topic). With that in mind, frameworks and technologies like React (and it’s 100s of opinionated frameworks like Next/Gatsby/…) and Vue surged in popularity so that developers didn’t have to work with just vanilla JS. So let’s take a look at some of the popular ones with the context of Headless CMS.

*Note: these aren’t code examples or guides, they’re just meant to serve as a bit of a guidance on how and why Headless CMS work with some popular frameworks. For a technical dive into working with them and DatoCMS,* [*for example with NextJS*](https://www.datocms.com/docs/next-js.md)*, check out our docs.*

## React CMS

### What is React?

Similar to GraphQL, React also has its origins at Facebook in the form of a software engineer called Jordan Walke. He was inspired by the functional programming language Lisp and wanted to create a component-based approach for devs to build applications.

React is an open-source JS library widely used for building user interfaces and large web applications that can update and render data efficiently without reloading the page. React’s core philosophy revolves around the concept of reusable components, enabling developers to break down complex UIs into simpler, isolated pieces of code. This modularity makes it easier to manage and scale web applications, while also enhancing readability and maintainability of the codebase.

If you’re keen, the folks over at Honeypot made a pretty [incredible documentary about React](https://www.youtube.com/watch?v=8pDqJVdNa44).

### Advantages of building with ReactJS

The most notable thing about React is its component-based architecture which promotes reusability and simplifies the development process.

React's virtual DOM (Document Object Model) ensures optimal rendering performance, making it a great choice for high-traffic applications. React’s popularity ([crazy active repo](https://github.com/facebook/react)) also means a vast ecosystem, with a wealth of libraries and tools available. Furthermore, its unidirectional data flow helps in creating more predictable and easier-to-debug code. React’s flexibility allows it to integrate seamlessly with various backends, making it a great choice for diverse web development projects.

### Why use a Headless CMS for React applications?

Using a Headless CMS with React capitalizes on the strengths of both. React’s frontend approach in building dynamic, responsive UIs pairs well with the backend efficiency of a Headless CMS. This combination facilitates a smooth content management process, where content updates from the CMS are reflected instantly on the React-driven UI without page reloads (depending on the website build, of course). The decoupled nature means developers can leverage React's rich interface capabilities while relying on the Headless CMS for robust content management and delivery.

## Vue CMS

### What is Vue?

VueJS and React represent two popular yet distinct approaches to building web interfaces in the JavaScript ecosystem. While React focuses on the efficient rendering of UI components, VueJS is a progressive framework that aims to be more approachable and versatile.

VueJS combines reactive data binding and composable view components with an API that’s considered to be more intuitive than React’s. React’s strong emphasis is on performance and large-scale application architecture, and could have a steep learning curve. In contrast, VueJS flexxes its simplicity and ease of integration into existing projects, making it a favorite for both small-scale applications and rapid development scenarios.

Either way, React and Vue (and Angular, which is what led to the creation of Vue) are arguably the most popular abstractions/frameworks/libraries, as you prefer to call them, of JavaScript, which have given birth to hundreds of other opinionated frameworks under them.

Similar to ReactJS, the folks over at Honeypot made a pretty cool [documentary about Vue](https://www.youtube.com/watch?v=OrxmtDw4pVI) as well!

### Advantages of VueJS

VueJS offers a gentle learning curve with a design that is both intuitive and empowering for developers. Its core library focuses on the view layer only, making it easy (subjective) to pick up and integrate with other libraries or existing projects. VueJS also features a reactive and composable data model that simplifies the process of building interactive UIs. The framework is designed to be incrementally adoptable, with layers of complexity that can be added as needed. This flexibility, combined with very well maintained docs and a massive community, makes VueJS a robust choice for modern web development.

### Working with a Headless CMS for VueJS

Similar to React, integrating a Headless CMS with VueJS brings the best of both worlds. Vue’s reactive data handling complements the API-first nature of a Headless CMS, allowing for seamless updates and rendering of content. This pairing enables devs to efficiently manage and publish content, and the modular architecture of VueJS aligns well with the reusable-content approach of a Headless CMS.

## NextJS CMS

### What is NextJS?

Remember when we said React/Vue were JS frameworks? Now let’s get into some Inception. NextJS is arguably\* the most popular React framework - more specifically, an open-source JavaScript framework built on top of React. NextJS was created by Guillermo Rauch, who then went on to found Vercel, and it is among THE most documented, active, and popular React frameworks out there, with an incredible team and community maintaining it.

NextJS enables functionality such as server-side rendering and generating static websites for React-based web applications. One of Next.js's key features is its ability to perform pre-rendering. It pre-renders every page by default, which means that each page's HTML is generated in advance, not just on client-side JavaScript. This approach significantly improves performance and SEO capabilities. Next.js also offers features like file-based routing, automatic code splitting, and optimized prefetching, making it a powerful and efficient framework for building complex web applications.

### Why is NextJS so adopted?

Its server-side rendering capabilities improve the performance and SEO of web applications, a crucial factor in today's digital world, this much we know. Aside from that, the framework's ease of setup and automatic optimizations like code splitting and prefetching make it highly appealing for developers looking to create fast, efficient applications with minimal configuration. Additionally, Next.js's versatility in building both static and dynamic websites, along with its rich ecosystem and community support, have solidified its place as a go-to framework for modern web development.

Between SWR, pre-fetching, AB Testing on the edge, custom middleware, and tons of other features, it really is incredible how much diversity NextJS can handle when it comes to development use-cases.

### Working with a NextJS Headless CMS

Integrating a Headless CMS with Next.js brings unique advantages. Next.js's server-side rendering and static generation features, combined with a Headless CMS, can significantly enhance the performance and SEO of a website. The CMS manages and stores the content, while Next.js handles the presentation and delivery of this content, just like with React.

This separation leads to better scalability and maintainability. Additionally, the flexibility of the CMS allows for dynamic content updates without the need for redeploying the Next.js application when using capabilities like real-time subscription APIs. This integration paves the way for more interactive and personalized user experiences, as developers can leverage Next.js's capabilities to dynamically render content based on user interactions and edge functions.

PS, the website for DatoCMS is built using NextJS, and it’s open source. So if you want to check out how NextJS looks like in action, [check out the repo](https://github.com/datocms/new-website)!

## Laravel CMS

Admittedly PHP’s become a bit of a punching bag in the CMS world when comparing to WordPress, but there’s still a lot of use-cases when PHP is and should be the go-to. JS has React, PHP has Laravel.

Laravel is a free, open-source PHP web framework, created by Taylor Otwell, intended for the development of web applications following the model-view-controller (MVC) architectural pattern. Known for its elegant syntax, Laravel simplifies many common tasks used in web projects, such as routing, authentication, caching, and sessions.

It aims to provide a great DX without sacrificing application functionality. Laravel also offers a robust ecosystem with tools like Forge and Vapor for deployment, and Nova for administration. Its extensive documentation and active community support make it a go-to choice for many PHP developers.

Integrating a Headless CMS with Laravel amplifies these benefits. Without sounding too repetitive, Laravel’s backend prowess, combined with the flexibility of a Headless CMS in content delivery, ensures a scalable, maintainable, and seamless content management and delivery process. This integration is particularly beneficial in scenarios where content needs to be served across multiple platforms or where the content strategy evolves independently of the application's architecture.

## Astro CMS

What is AstroJS? Astro is one of the cool (like really cool) new kids on the block, from as recent as 2021. AstroJS is designed as a modern web framework for building faster and more efficient websites combining the best aspects of static site generation with a component-based architecture with a unique twist.

It stands out by delivering a zero-JavaScript-by-default approach, which drastically reduces the amount of JS sent to the browser. This feature is particularly beneficial for developers aiming to build high-performance websites with improved loading times.

AstroJS supports component-based architectures, allowing you to write UI components in your favorite frameworks like React, Vue, or Svelte, but it only ships the necessary JavaScript to the client. This selective hydration strategy ensures that users load only the JavaScript they need, it’s really cool.

Using a Headless CMS with AstroJS can be a powerful combination. The Headless CMS serves as the content backbone, managing and delivering content via APIs. AstroJS, on the other hand, efficiently handles this content on the frontend. The pairing is especially effective for sites that require frequent content updates while maintaining high performance, such as blogs, news sites, and portfolio pages.

## NuxtJS CMS

React has Next, Vue has Nuxt.

NuxtJS is a progressive framework based on VueJS, designed to simplify the development of modern web applications. It extends VueJS by providing an opinionated structure for building scalable and performant applications, particularly enhancing the creation of universal or server-side rendered applications.

NuxtJS streamlines the development process with features like automatic code splitting, easy page routing, and server-side rendering, which are vital for improving website load times and SEO performance. Its pre-configured setup, including Vue Router, Vuex store, and Vue Server Renderer, allows developers to focus more on building the application rather than on setup and configuration.

Integrating a Headless CMS with NuxtJS can significantly benefit web projects. The framework's server-side rendering capability ensures that content from the CMS is rendered efficiently, enhancing both performance and SEO. This combination is particularly effective for content-driven sites where frequent updates and fast content delivery are essential. NuxtJS's modular architecture, combined with the flexibility of a Headless CMS, offers a scalable solution for building diverse web applications, from e-commerce sites to blogs and corporate websites, allowing for dynamic content updates while maintaining high performance and user experience.

## Remix CMS

Remix is a relatively new one in the field of web development frameworks, and it's quickly gaining traction for its unique approach and robust features. Also built on React, Remix extends and enhances the capabilities of a traditional React application, focusing on better user and developer experiences.

Remix stands out by emphasizing web fundamentals, offering a tighter alignment with the way the web has been designed to work. This means improved handling of various aspects like data loading, caching, and prefetching. It advocates for server-side rendering and progressive enhancement, ensuring that applications are fast and accessible.

One of Remix's core principles is to bring back the robustness of traditional web request-response cycles, integrated smoothly with modern React UI capabilities. Remix also simplifies data fetching and form submission, making it easier to manage state and data flow in React applications.

When paired with a Headless CMS, it opens up possibilities for creating performant, SEO-friendly, and content-rich digital experiences.

## Svelte CMS

### What is Svelte?

Svelte is a bit of an outlier here - because it isn’t a derivative of React, Vue, or Angular. Svelte stands out in the landscape of JS frameworks for its unique approach to building UIs. Unlike React, Vue, or Angular, Svelte shifts much of the work to compile time, rather than relying on a virtual DOM or runtime abstractions. This results in Svelte applications directly manipulating the DOM when the state changes, leading to faster performance and less code.

While frameworks like React and Vue provide a layer of abstraction over the DOM through a virtual DOM, Svelte compiles components into imperative code that updates the DOM. This means that, unlike traditional frameworks that do most of their work in the browser, Svelte does its heavy lifting at build time. By removing the need for a virtual DOM, Svelte applications typically have smaller bundle sizes and faster runtime performance, making it one of the most popular frameworks to come out in the recent past that aren’t built upon the big 3.

### Advantages of Svelte

Enhanced performance is one of the most notable advantages of Svelte. By eliminating the virtual DOM, Svelte applications offer superior load times and smoother updates. The framework's syntax simplicity is another benefit, making it accessible and easy to learn for devs, and resulting in more readable and maintainable code.

Svelte’s compiled nature leads to reduced bundle sizes, which is a crucial factor in web performance, especially for mobile users and in low-bandwidth situations. Additionally, Svelte offers a more intuitive model for reactivity. Unlike other frameworks that require specific patterns or libraries for state management, Svelte allows developers to update the state simply by assigning values, streamlining the development process.

### Growing Svelte Ecosystem and Content Management

There’s a growing ecosystem out there for Svelte. Most notably, SvelteKit, an upcoming framework built around Svelte, aims to be the 'next-gen' platform for building more complex applications. It's designed to handle server-side rendering, static site generation, and single-page applications, all within the Svelte ecosystem. SvelteKit reflects the growing maturity of Svelte, offering a more comprehensive set of tools for developers to build full-fledged web applications.

Aside from SvelteKit, names like Routify, ElderJS (SSG for Svelte, pretty cool!), Sapper, Vite, and Rollup are recently familiar. Just like the React ecosystem, the Svelte one is definitely one to keep an eye on as the community grows and gets more active.

Which is why, of course, a Headless CMS with Svelte can be a great combo. Without repeating the same thing over and over again, Svelte apps are meant to be quick and scalable, regardless of whether it’s a portfolio or an eCommerce site, so having a Headless CMS API that focuses on performance is a dream pairing for Svelte apps.

---

# Modern Web Development Concepts

Source [academy]: https://www.datocms.com/academy/modern-web-development/modern-web-development-concepts.md

Understand some common use-cases, APls, and frameworks that go into building modern web experiences with Headless CMS

SSR, SSG, ISR, and CSR—if these sound like obscure government agency abbreviations instead of web rendering technologies, we’re here to clarify some of the confusion.

SSR, SSG, ISR, and CSR are actually the four main techniques used for rendering web pages. While they might seem like alphabet soup to some, they're fundamental concepts in the realm of modern web development.

## **SSR: Server-Side Rendering**

Server-Side Rendering (SSR) is a rendering technique where web pages are generated on the server on demand and then sent to the client's browser. In simpler terms, imagine you're hungry and decide to order a pizza.

With SSR, the pizza is fully cooked in the kitchen (server) and then delivered to your doorstep (browser) ready to eat, rather than the chef bringing all the ingredients over to your house and cooking them in your kitchen. Sure the latter might get to your house faster, but you’re going to be waiting and using your own resources to contribute to the preparation of the final product.

Here's how SSR works: when a user requests a web page, the server fetches the necessary data, processes it, and generates an HTML document dynamically. Once the HTML content is ready, it's sent to the client's browser, where it's displayed to the end user.

For example, imagine visiting a news site built with SSR. When you click on a news, the server retrieves the article's content, assembles it into HTML format, and sends it to your browser. This means that you see the full article immediately, without having to wait for annoying client-side processing.

#### **Pros:**

-   Improved initial load time
-   Better SEO
    
-   Content is accessible even if JavaScript is disabled, allowing for wider browser compatibility
    

#### **Cons:**

-   Increased server resource usage
-   More round trips to the server
    
-   Limited client-side interactivity
    

### **Best practices when working with SSR**

-   **Optimize server response time**: Optimize database queries, retrieve only the minimum amount of required data via API calls, and efficiently leverage server-side rendering frameworks (e.g., by devouring the latest content available [in our academy](https://www.datocms.com/academy.md)!) to minimize page generation time.
-   **Optimize Google's Core Web Vitals**: Monitor key performance and user experience metrics such as First Contentful Paint (FCP), Interaction To Next Paint (INP), and Cumulative Layout Shift (CLS) to identify areas for optimization with tools like [Google Lighthouse](https://developer.chrome.com/docs/lighthouse/overview).
    
-   **Add SEO metadata to your pages**: Ensure that your server-rendered content is fully accessible to search engine crawlers and that each page has the appropriate metadata according to SEO best practices.
-   **Consider server-side caching**: Adopt caching mechanisms at the server or database level with tools like [Redis](https://redis.io/) to save time when receiving requests for already rendered pages.
    
-   **Monitor and optimize server performance**: Avoid server overloads by keeping an eye on server-side rendering performance metrics through application monitoring systems.
-   **Build 404 and 500 error pages**: Implement robust error handling mechanisms and fallback strategies to gracefully handle rendering failures and server-side errors.
    

## **SSG: Static Site Generation**

Static Site Generation (SSG) involves pre-building all website pages at build time, rather than generating them dynamically on each request.

Think of SSG as a nice dinner party where you have already prepared all the dishes in advance to have more time to have fun with your friends during the event (or maybe you bought something pre-packaged because you’re lazy ). When your guests arrive (website visitors), they can browse the tables (static site) and see all the dishes (web pages) laid out neatly. Quite efficient, isn't it? You don't even need a cook (server) to cook the dishes (generate content) on the spot.

For example, a blog based on SSG would have its blog post pages compiled into HTML files during the build process. If you visit that blog, you’ll receive pre-rendered HTML pages instead of having to wait for on-demand server-side rendering.

#### **Pros:**

-   High focus on speed and performance
-   Increased security
    
-   No need for a complex web server
    

#### **Cons:**

-   Limited interactivity
-   Longer build times
    
-   Content updates require new builds
    

### **Best practices when working with SSG**

-   **Use CDN for content delivery**: Distribute your pre-rendered pages closer to end-users by uploading them to a CDN, reducing latency and page load times across geographically dispersed locations.
-   **Optimize build times**: Streamline your static site generation process to minimize build times, ensuring quick updates and deployments.
    
-   **Prefer lightweight frameworks**: Consider frameworks and libraries that don’t take up much space, as no one loves to wait for their device to download and load a web page.
-   **Structure your site for UX and SEO**: Design the site structure to facilitate navigation by users and crawling by search engines, maximizing both user experience and visibility. [Discover more](https://www.datocms.com/academy/headless-cms/headless-cms-and-seo.md).
    

## Static Sites

Now, with Jamstack becoming a norm for most modern websites, web pages have become increasingly complex and offer richer user experiences. Server-side rendering involves creating pages on the fly when a request arrives on the server. This takes time and slows down the response time.

Enter Static Sites and Static Site Generators. They allow the creation of pre-generated pages at build time, serving up pre-built HTML, CSS, and JS files, and offering better security, performance, and SEO opportunities.

### What is a Static Site?

A [static site](https://en.wikipedia.org/wiki/Static_web_page) consists of a collection of prebuilt HTML documents, CSS styles, and JavaScript files. When a client makes a request for one of the pages in a static site, the hosting servers directly return the pre-built HTML documents associated with the given URL.

Thus, the hosting delivers the website files to visitors' browsers exactly as they appear on the server. Then the user's browser renders the pages and executes the JavaScript scripts on the client.

This mechanism greatly reduces server response times. The reason is that, unlike dynamic sites, there is no need for retrieving data from a database and generating pages on the fly. Instead, everything is static and already embedded in HTML pages saved on the hosting server disk or memory. In other words, serving a static website requires no server-side processing or connection to a database.

If you want to update such a site, you must regenerate individual pages to update their content. This may require a new deploy. While this process can be cumbersome on large sites, static sites tend to provide faster navigation times and better technical SEO metrics.

### Why use a Static Site Generator?

As you can imagine, managing a static site takes a lot of effort. For example, suppose you want to update a section of the template shared across all pages. To do so, you would have to manually change each individual HTML document the site consists of. In a large project, that is simply not possible. Here is why you should use instead a static site generator system.

A static site generator, usually abbreviated to SSG, is a software tool to generate static websites. In detail, it automates the process of converting pages written in markup languages like Markdown or programming languages like JavaScript into a collection of HTML, CSS, and JavaScript files that make up a static site.

The main benefits introduced by static site generators are:

-   **Streamlined development process**: An advanced SSG technology lets code your static site with popular technologies such as React, Vue, and Angular. In addition, it usually provides simplified route management. This helps you organize your page components into a directory structure that will reflect the structure of the output website.
-   **Support for templates and UI libraries**: To define the layout and structure of a site, SSGs use an approach based on templates. For instance, Next.js offers in-depth [layout templating features](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts). These templates typically include reusable components like headers, footers, navigation menus, and others. If you opt for a JavaScript static site generator, you also have access to all the packages in the npm ecosystem and well-known UI component libraries such as [Bootstrap](https://react-bootstrap.github.io/), [Ant Design](https://ant.design/), and [Material-UI](https://mui.com/).
    
-   **Simplified page generation**: When you run the build command in the static site generator, the application parses the source files. For each page, it retrieves the required data via queries or API calls, embeds it in the page's HTML document, and produces the rest of the necessary CSS and JavaScript files. So, it generates static HTML files applying any formatting, styling, or other transformations specified in the templates.
-   **Optimized output files**: The collection of static HTML, CSS, JavaScript, and asset files produced by the static site generators are typically compressed, minimized, and optimized for the Web. This helps the user save network resources to retrieve them and client resources to render them.
    
-   **Easy deployment**: Once the static site has been generated, you can deploy the produced files to any web server, CDN (Content Delivery Network), or static site hosting service. Advanced static site generators like Next.js also have a built-in web server to serve static files directly.
    

### Examples of SSGs

That being said, there's several options to choose from when building out static sites. Some of the most commonly found examples are:

-   **Next.js**: A Node static site generator that allows you to write static sites in React. As of this writing, it is one of the most widely used web technologies in the world, with a [market share of 16.6%](https://gohugo.io/content-management/front-matter/).
-   **Astro**:A fresh technology that combines decades of proven performance best practices with the DX improvements of web components. It allows you to use your favorite JavaScript frameworks like Vue.js or React when building a static site.
    
-   **Nuxt**: A web framework for developing static web applications using Vue.js. It supports both static site generation and server-side rendering. In both cases, you only need to write web pages and Nuxt will do the rest. Find out [how to build a blog in Nuxt](https://www.datocms.com/blog/how-to-build-a-nuxt-blog.md).
-   **SvelteKit**: A framework for building static web applications, with a seamless development experience, and flexible file system-based routing. Follow our tutorial on [how to build a blog in SvelteKit](https://www.datocms.com/blog/how-to-build-a-svelte-blog-with-a-headless-cms.md).
    

You can find the list of the other open-source static website generators on the [official Jamstack site](https://jamstack.org/generators/).

## **ISR: Incremental Static Regeneration**

Incremental Static Regeneration (ISR) is an advanced technique to update specific parts of a statically generated site without having to rebuild the entire thing.

Consider your site as a bustling city, constantly evolving and expanding. ISR is like having a team of urban planners continuously updating and improving your city without disrupting its daily life. It's like renovating one building in the city while the rest of the metropolis remains untouched. Italian [*umarels*](https://en.wikipedia.org/wiki/Umarell) would love that! (elderly Italians love to stare at construction sites).

(Image content)

When a user requests some web page content—if it’s been a while since its last update—ISR springs into action to swiftly regenerate the page with fresh data and return it to the client. What an innovative way of keeping your site fresh and relevant!

For example, consider a platform where service prices change frequently. With ISR, only the service pages needing updates are regenerated, ensuring you always see up-to-date prices information.

#### **Pros:**

-   On-demand updates
-   High scalability
    
-   A mix between SSR and SSG
    

#### **Cons:**

-   Difficult to implement
-   Hard to find the right regeneration criteria
    
-   Increased complexity
    

### **Best practices when working with ISR**

-   **Focus on critical paths and sections:** Get the most out of ISR by prioritizing the pages or sections of your website that require frequent updates or personalized content.
-   **Optimize revalidation intervals**: Fine-tune the revalidation intervals for ISR to balance between serving fresh content and minimizing server load, considering factors like content volatility and user traffic patterns.
    
-   **Implement content fallback strategies**: Establish fallback mechanisms to serve stale content during regeneration periods, ensuring a smooth user experience even when fresh data is unavailable.
-   **Set cache control headers**: Configure [cache control headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) to instruct clients on how to locally cache ISR responses to avoid triggering regenerations with unnecessary requests.
    

## **CSR: Client Side Rendering**

Client-Side Rendering (CSR) is a web development approach where the rendering of web pages occurs dynamically on the user's device rather than on the server.

Assume the Web is an interactive canvas. You can think of CSR as the artist who sketches the details directly on your screen . Now, imagine walking into an art gallery where each painting is blank upon arrival. As you approach a painting, the details gradually emerge, meticulously crafted by the artist right before your eyes. Similarly, in CSR, the webpage arrives as a blank canvas, and your browser uses JavaScript to fetch data and construct the visual elements on the fly.

For instance, consider a Single Page Application (SPA) that relies on CSR, such as a social media platform. If you visit it—instead of receiving fully rendered pages from the server—your browser receives empty pages with involving a JavaScript bundle. The browser executes the JS code, dynamically generates the DOM, and retrieves new content as you navigate the app.

#### **Pros:**

-   Enhanced interactivity
-   Improved reactivity
    
-   Rich UI
-   Resources can be requested onlt when they need to be rendered
    

#### **Cons:**

-   Resource-intensive rendering tasks that take time
-   SEO challenges
    
-   Requires JavaScript enabled
    

### **Best practices when working with CSR**

-   **Minimize bundle size**: Improve load times and reduce bandwidth consumption by optimizing the size of client-side bundles with tree shaking, code minification, and code splitting in small chunks.
-   **Implement lazy loading**: Defer the loading of non-essential resources until they’re needed, reducing initial page load times and conserving bandwidth.
    
-   **Cache assets strategically**: Leverage browser caching to cache static assets and API responses like a boss.
-   **Optimize client-server communication**: Streamline client-server communication by minimizing unnecessary requests and compressing JSON data payloads.
    
-   **Ensure browser compatibility**: Test your application across different browsers and devices to ensure consistent performance, responsiveness, and functionality.
    

## SSR vs SSG vs ISR vs CSR

Thought of SSR, SSG, ISR, and CSR as mutually exclusive rendering approaches? Think again!In reality, all four rendering methods can be employed together, each on different areas of the same site:

-   **Server-Side Rendering (SSR)**: Pages in the e-commerce section of the site.
-   **Incremental Static Regeneration (ISR)**: Pages in the blog section of the site.
    
-   **Static-Site Generation (SSG)**: Home page, landing pages, terms and conditions page, and privacy policy page.
-   **Client-Side Rendering (CSR)**: Back-office SPA platform.
    

  
Moreover, even within a server-generated page, certain sections may require client-side rendering and JavaScript execution. Thus, the landscape of page rendering is far more diverse than you may have thought!

Confront the four different rendering approaches explored here in the final SSR vs SSG vs ISR vs CSR summary table below.

| Criteria | SSR | SSG | ISR | CSR |
| --- | --- | --- | --- | --- |
| Rendering Location | Server | Server | Server | Client |
| Rendering Time | On-request arrival | At build time | At build time + On-request arrival updates | On receiving page in the browser |
| Dynamic Updates | ✅ | ❌ | ✅ | ✅ |
| SEO Friendly | ✅ | ✅ | ✅ | ❌ |
| JS Execution | Not Required | Not Required | Not Required | Required |

---

# Content Management APIs

Source [academy]: https://www.datocms.com/academy/modern-web-development/content-management-apis.md

Understand some common use-cases, APls, and frameworks that go into building modern web experiences with Headless CMS

API-first content management fundamentally reverses the traditional CMS development approach, something we’ve covered in previous chapters on the emergence of Headless CMS. Instead of designing the CMS and then adding APIs as an afterthought, the API-first strategy starts with creating robust and flexible APIs to manage content and assets. These APIs become the foundation upon which the rest of the CMS is built.

## The Emergence of API-First CMS

The rise of API-first CMS was a response to the growing need for omnichannel content delivery. When content needs to be seamlessly distributed across websites, mobile apps, IoT devices, and more, traditional CMSs fall short. They are often rigid and not well-suited for the fluidity and scalability required for modern ecosystems. API-first CMS, with its decoupled architecture, addresses these challenges, offering unparalleled flexibility and scalability.

Probably worth clarifying here that API-first CMS and Headless CMS are basically the same thing - but blame our marketing guy for wanting to create a whole new section for this just for SEO.

## Best Practices in API-First Content Management

Before starting to look at the common API types and get into technical specifics, let’s establish some foundational best practices for getting the most out of adopting the Headless CMS approach when it comes to API design.

This is relevant for *designing* content APIs, but if you’re selecting a Headless CMS, then you should make sure that your vendor is following these guidelines as part of your evaluation.

### **Designing Scalable APIs**

Ensure that the APIs are scalable and can handle the expected load as your content and audience grow. This isn’t only applicable to the sheer volume of data the APIs can deliver in terms of content and assets, but also the query complexity that can be handled based on your projects’ schema and structure.

### **Prioritizing Security**

Implement robust authentication and authorization protocols to secure access to your APIs. While the APIs themselves should always encrypt data in rest and transit, they should also allow for varied access control through user authentication, roles, and PAT (auth tokens) permissions.

### **Building for Flexibility**

Create APIs that are flexible enough to accommodate future changes in content structure and front-end technologies.

### **Ensuring Consistent Documentation**

Maintain comprehensive and up-to-date API documentation to ensure ease of use for developers. Docs are a core feature to the adoption of APIs by providing a better DX, and this should always be a consideration when designing APIs, not an afterthought.

### **Focusing on Performance**

Optimize APIs for performance, ensuring fast content delivery. No one likes to wait for an extra 100ms, so ensuring that responses are cached, delivered quick, and well optimized go a long way in designing performant APIs.

Now that we’ve got the basics in place, let’s dive into the most common API types you’d find in the CMS world.

## REST APIs

## What is a REST API?

REST (Representational State Transfer) is an architectural style that conforms to a set of constraints when developing web services. It was introduced as a successor to SOAP APIs. REST, or RESTful APIs, are Web Service APIs that follow the REST standards. SOAP were restricted to delivering data in XML formats, and as the need for better structured data responses became prevalent in modern development, REST came about as a successor that primarily provided JSON. While REST can still support providing data in XML, HTML, or YAML, the most common format remains JSON.

When a client calls REST APIs the server transfers the resources in a standardized representation. They work by returning information about the source that was requested - and is translated into an easier to consume format - those JSON curly bois.

REST APIs also allow for modifications and additions from the client-side to the server.

## Working with REST APIs

A REST request is made up of the endpoint, HTTP method, Header, and Body.

An endpoint contains a URI (Uniform Resource Identifier) that helps in identifying the resource online.

REST APIs work on using HTTP verbs to perform CRUD operations:

1.  POST: to Create
    
2.  GET: to Read
    
3.  PUT: to Update (replace)
    
4.  PATCH: to Update (modify), and
    
5.  DELETE: to Delete.
    

Headers provide information to clients and servers for purposes like caching, AB Testing, authentication, and more.

The body contains information that a client wants to send to a server, such as the payload of the request.

## GraphQL

### What is GraphQL?

Quite simply put, GraphQL is a query language for APIs and a runtime for fulfilling those queries with existing data. GraphQL offers a comprehensive and clear outline of the data within your API. It empowers clients to precisely request what they require, without excess, simplifies the evolution of APIs over time, and supports the development of potent tools for developers.

GraphQL is versatile in its functions, encompassing capabilities for reading, writing (mutations), and subscribing to data changes (real-time updates). It offers servers in various programming languages, such as Haskell, JavaScript, Perl, Python, Ruby, Java, C++, C#, Scala, Go, Erlang, PHP, and R.

The appeal of GraphQL largely comes from its core principle: requesting precisely what you need and receiving exactly that – no more, no less. This approach allows for highly predictable responses from your API queries, eliminating issues of overfetching or underfetching.

GraphQL APIs are organized in terms of types and fields, not endpoints, making them extremely easy to get up and running, since you can access all of your data from a single endpoint. GraphQL uses types to ensure apps only ask for what’s possible and provide clear and helpful errors. Apps can use types to avoid writing manual parsing code.

The folks over at Honeypot made a [great documentary on GraphQL](https://www.youtube.com/watch?v=783ccP__No8&list=PLtEPUaeDclku1ECmuN3IsUimHApukWIOf&index=8) if you're keen to dive in!

## Others

REST and GraphQL aren’t the end of the content API spectrum, but they’re definitely the most commonly used. At times you may encounter other API types and extensions as well, and while we at DatoCMS don’t provide APIs in these specs, it’s probably worth knowing a little about them and why they exist.

### SOAP (Simple Object Access Protocol)

SOAP is a protocol for exchanging structured information in web services. In content management, SOAP can be used for inter-application communication, enabling different systems to interact and exchange content data seamlessly. It is known for its high security and extensive standards, making it suitable for enterprise-level applications where robustness and formal contracts are required.

### gRPC (gRPC Remote Procedure Calls)

gRPC, a modern open-source framework, uses HTTP/2 to provide highly efficient and scalable communication between services. In content management, gRPC can facilitate rapid content synchronization across various microservices, thanks to its low-latency and language-agnostic nature. It's particularly useful for real-time content updates and complex transactional systems.

### OData (Open Data Protocol)

OData is a standard protocol for creating and consuming RESTful APIs. It's used in content management to standardize the querying and manipulation of data, allowing for more flexible data access from various client platforms. OData supports complex querying capabilities, making it easier to handle sophisticated content retrieval requirements.

### JSON-RPC (JSON Remote Procedure Call)

JSON-RPC is a lightweight protocol that allows for remote procedure calls using JSON as its format. In the context of content management, it enables simple and efficient API calls for CRUD operations on content, with minimal overhead. It's favored in scenarios where speed and simplicity are more critical than comprehensive features.

### XML-RPC (XML Remote Procedure Call)

XML-RPC is a protocol that uses XML to encode remote procedure calls. In content management systems, XML-RPC can be used for system interoperability, allowing different CMS platforms to communicate and exchange data. Its simplicity and wide adoption make it a viable option for basic content management tasks, especially in legacy systems.

---

# GraphQL Vs. REST

Source [academy]: https://www.datocms.com/academy/modern-web-development/graphql-vs-rest.md

Understand some common use-cases, APls, and frameworks that go into building modern web experiences with Headless CMS

We’ve established that GraphQL and REST are two popular, yet distinct approaches to designing APIs for exchanging data. REST enables client apps to exchange datausing HTTP, which is the standard communication protocol. On the other hand, GraphQL is an API query language that defines specifications of how a client app should request data from a server. You can use GraphQL in your API calls without relying on the server-side application to define the request. So before diving into the differences between the two, let’s take a quick look at what they share in common.

## Similarities between REST and GraphQL

### **Architecture**

Both, REST and GraphQL, are stateless, so the server doesn’t save response histories between requests. And since they’re both using client-server models, requests from a single client results in replies from a single server (i.e. one query hits one endpoint).

### **Resource-based Design**

REST and GraphQL both design their data interchange around resources, which is any data or object that the client can access through the API. Each resource has its own unique identifier and a set of operations that the client can perform on it.

For example, consider a menu API where users create and manage products. In a resource-based API, a product could be a resource. It has its own unique identifier, for example, `/menu/pizza-diavola`, and it has a set of operations, such as `GET` to retrieve the product in REST or a query to retrieve the product in GraphQL.

### **Language and database neutrality**

Both REST and GraphQL support similar data formats, more importantly, open ones, since neither generates proprietary gibberish in their standard applications. JSON is the most popular exchange format that all languages, platforms, and systems understand. The server returns structured JSON to the client. At times, some other data formats like XML and HTML are available but less common.

That being said, GraphQL emerged as a successor to REST (more specifically, by Facebook, to sort out their mobile app performance issues) to try and overcome some of its limitations, most notably, Fixed-structure data exchange, and the issue of Over/Under-fetching.

REST requires client requests to follow a fixed structure to receive a resource. This rigid structure is easy to use, but it’s not always the most efficient means to exchange exactly the data needed.

Further, REST always returns a whole dataset. For example, from a product object in the REST API, you would receive everything the endpoint has about the product (like the price, ingredients, pictures, etc.) even if you just wanted to get the pizza’s name.

If you wanted to know the pizza’s ingredients and price, you would need multiple API requests. GraphQL emerged as a query-based solution. Queries can return the exact data in only one API request and response exchange.

So let’s take a look at the differences.

## Difference between REST and GraphQL

The core difference between GraphQL and REST APIs is that GraphQL is a specification, a query language, while REST is an architectural concept for network-based software. GraphQL operates over a single endpoint using HTTP.

GraphQL is great for being strongly typed, and self-documenting based on schema types and descriptions and integrates with code generator tools to reduce development time. In addition, REST development has been more focused on making new APIs. Meanwhile, GraphQL’s focus has been on API performance and flexibility.

Here’s some of the criteria where we see notable differences, saving the best for last.

### Client-side Requests

So how do REST requests actually work?

-   First, you’ve got HTTP verbs that determine the action, like `GET`.
-   Then, there’s a URL that identifies the resource on which to action the HTTP verb, like the menu endpoint from where you can `GET` the data.
    
-   And finally, parameters and values to parse, if you want to create or modify an object within an existing server-side resource.
    

For example, you use `GET` to get read-only data from a resource, `POST` to add a new resource entry, or `PUT` to update a resource.

In contrast, here’s what GraphQL requests use:

-   A query for getting read-only data
-   Mutations for modifying data in the database
    
-   And subscriptions to receive event-based or streaming data updates (listening for changes in real time rather than having to query for changes)
    

A data format describes how you would like the server to return the data, including objects and fields that match the schema. You can also input new data using Mutations. Under the hood though, GraphQL sends every client request as a POST HTTP request.

### Versioning

As APIs grow and evolve, their data structures and operations may change. For clients without the knowledge of these changes, it can break their systems or introduce unknown errors.

REST APIs often include versioning in the URL to solve this issue, like https://example.com/api/v1/menu/12341. However, versioning is not mandatory, and it can lead to errors.

GraphQL requires API backward compatibility. So deleted fields return an error message, or those with a deprecated tag return a warning.

### Error Handling and Type Safety

GraphQL is a strongly typed API architecture, which means that it requires a detailed description of the data, its structure, and data operations in the schema. Due to the level of detail in the schema, the system can automatically identify request errors and provide useful error messages.

REST APIs are weakly typed, and you must build error handling into the surrounding code. For example, if a PUT request parses a number value as text instead of as an integer, the system does not identify the error automatically.

### Schema

GraphQL uses a strongly typed system to define the capabilities of an API. All the types that are exposed in an API are written down in a server-side schema using the GraphQL Schema Definition Language (SDL) and/or code-first, including details like:

-   Object types and fields that belong to each object (think content models and fields in the CMS abstraction).
-   Server-side resolver functions that define an operation for each field
    

The schema explicitly defines types to describe all data available on the system and how clients can access or modify that data. On the other hand, REST APIs do not require a server-side schema. But you can optionally define it for better API design, documentation, and development.

Frontend teams can now work with the typed GraphQL API knowing that if any changes occur from the backend team on the APIs design, they’ll get this instant feedback when querying it from the frontend.

### Data Fetching

One of the common limitations of REST out-of-the-box is that of overfetching and underfetching. This happens because the only way for a client to download data is by hitting endpoints that return fixed data sets. It’s very difficult to design the API in a way that it’s able to provide clients with their exact data needs.

Under REST architectures, data is returned to the client from the server in the whole-of-resource structure specified by the server. The following examples show returned data in REST and GraphQL.

With REST, you’re likely to get something like this when hitting the endpoint for pizzas with a `GET /pizzas` operation:

```json
[
  {
    "id": 1,
    "title": "The Diavola",
    "description": "Spicy and juicy"
  },
  {
    "id": 2,
    "title": "The Margherita",
    "description": "The classic”
  },
  {
    "id": 3,
    "title": "The Hawaiian”,
    "description": "Are you sure?"
  }
]
```

Whereas in contrast, GraphQL would provide something along these lines if you queried for specifics like the first pizza on the menu only using GET `/graphql?query{pizza{id: 1}}`:

```json
{
  "data": {
    "pizzas": [
      {
        "id": "1",
        "title": "The Diavola",
        "description": "Spicy and juicy."
      },
]}}
```

## REST vs. GraphQL

Ok, that’s fun, but what does the difference really look like in the use-case of using a Headless CMS, and why should you care?

When thinking of one of the most known differentiations - the differences in expected responses for queries - in very simple terms, let’s go eat a pizza.

Imagine you’re ordering pizza, and you’re craving a Margherita. Now if that was a RESTaurant (so sorry), you get every ingredient in that pizza every time, it’s always going to be the same shape and size.

Terminal window

```bash
https://example.com/api/pizza
```

If you were in a GraphQL restaurant though, you can have it your way. Don’t like basil? Take it off. Describe your pizza however you like. You’re vegan and don’t want cheese? That’s fine too.

```graphql
{
  Pizza {
    Crust
    tomatoSauce
    Cheese @skip(if: $vegan)
  }
}
```

Now some pizza enthusiasts would say you could’ve just ordered a PIzza Marinara, but you didn’t know that existed, and the GraphQL API still accommodated your request and gave you exactly what you wanted, no more, no less.

Conversely, if you wanted to gather some information from a specific endpoint, you couldn’t limit the fields that the REST API returns; you’ll always get a complete data set - or overfetching - when using REST APIs out of the box without added configurations.

GraphQL uses its query language to tailor the request to exactly what you need, from multiple objects down to specific fields within each entity. GraphQL would take x endpoint, and it can do a lot with that information, but you have to tell it what you want first.

Using the same example but for ordering pizzas online, the request would simply be to get `pizzaItem`, `pizzaToppings`, `pizzaImage`, and `pizzaPrice` from the same pizza model’s fields, within one request, and no more. All other content within the database wouldn't be returned, so the issue of overfetching wouldn't be a concern.

REST gets you the pizzas that the restaurant has on the menu, but GraphQL lets you modify that pizza to get exactly how much of what you want.

Opting for GraphQL against or with REST is a highly subjective decision, heavily influenced by the use-case. It is important not to consider GraphQL as an alternative to REST, nor as a replacement. To help simply those differences here’s a quick REST vs. GraphQL cheat sheet to refer to:

| REST APIs | GraphQL APIs |
| --- | --- |
| Been around since 2000 | Been around since 2012 |
| An architectural style largely viewed as a conventional standard for designing APIs | A query language for solving common problems when integrating APIs |
| Uses a server-driven architecture | Uses a client-driven architecture |
| Uses caching automatically | Lacks in-built caching mechanism |
| Supports multiple API versions | No API versioning required |
| Response output in JSON, but can also support YAML, HTML, and XML | Response output in JSON |
| Doesn't offer type-safety or auto-generated documentation | Offers type-safety and auto-generated documentation |
| Simplifying work with multiple endpoints requires expensive custom middleware | Allows for schema stitching and remote data fetching |
| REST is good for simple data sources where resources are well defined. | GraphQL is good for large, complex, and interrelated data sources. |
| REST returns data in a fixed structure defined by the server. | GraphQL returns data in a flexible structure defined by the client. |
| With REST, the client must check if the returned data is valid. | With GraphQL, invalid requests are typically rejected by schema structure. This results in an autogenerated error message. |

---

# Working with the DatoCMS MCP

Source [user-guides]: https://www.datocms.com/user-guides/content-management/working-with-the-datocms-mcp.md

14m 26s

If you've ever wished you could just *tell* DatoCMS what to do instead of clicking through it, the new DatoCMS MCP is exactly that. MCP stands for Model Context Protocol, which is a standard that lets AI assistants like Claude or ChatGPT connect directly to external tools like Dato. In our case, it means your AI assistant can create records, translate content, update fields, manage assets, and so much more, all from a simple conversation (like you're doing anyways, we can all stop pretending, myself included 😅).

The best part? You don't need to be a developer to use it. There's nothing to install, no API tokens to copy, and it works across Claude, ChatGPT, and even the mobile apps. If you can type a message (and copy paste a little URL), you can use the DatoCMS MCP.

To connect it, you'll go through a quick one-time OAuth login, which is just a fancy way of saying you click "Connect," log in with your DatoCMS account in the browser, and choose which projects the AI should have access to. That's it. Once you're connected, the assistant knows who you are and what it's allowed to touch, and you can get to work.

When you're adding a connector, all you need to know is:

-   Name: Put something simple (I used `DatoCMS`)
-   The MCP is hosted at `https://mcp.datocms.com` so that's the URL you'll have to add.
    

Once it's hooked up, the kinds of things you can ask it to do are pretty broad. You can ask it to create a new blog post with specific content, translate a batch of records into multiple languages, update a field across all records of a given type, move content through workflow stages, or even reorganise your media. It handles multi-step tasks well, so things that would normally take you 30 minutes of clicking around can often be done in a single request.

In this guide we walk you through creating a connection to DatoCMS using Claude, before letting Claude create a brand new project for us for a new Pizzeria based in Berlin. It handles the schema creation, the content creation, localization, and then we do a little workflow changes using Claude for Mobile to show how easy the entire workflow it.

The MCP is currently in beta, and we'd love to get your feedback.

[

(Image content)

Next episode

Understanding the Media Area in DatoCMS

](https://www.datocms.com/user-guides/media-management/understanding-the-media-area-in-datocms.md)

---

# Visual Editing in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/content-management/visual-editing-in-datocms.md

5m 50s

This video covers how you can use Visual Editing for your content in your DatoCMS project (with minimal configuration needed from your devs!)

Want a quick sneak peek into the feature? Here's the promo video that zips through the capabilities 👇

(Video content)

Visual Editing supports two workflows. Use either one, or both. You pick whichever fits the moment!

### Click-to-edit

This is the simplest setup (especially if you've been using Vercel's integration with Headless CMS in the past). You visit your website in draft mode, hover over content to see what's editable, and click to open DatoCMS in a new tab. It works entirely on your frontend, and with any hosting: Vercel, Netlify, Cloudflare, you name it. You don't need any plugins to be installed, nothing. Nada. Just Content Link (that's your dev's problem, not yours).

(Video content)

### Side-by-side edit

If you've been using the [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md)plugin in the past, this is that plugin on steroids.

The result is the workflow editors have been asking for since headless CMSes became a thing: preview on the left, edit panel on the right. Click on any piece of content, the edit panel opens right there. No tab switching, no context loss, instant live updates.

(Video content)

This plugin also enables you to have preview links in the CMS sidebar, have bidirectional navigation (scroll either panel, the other panel will keep up with context), AND give you full-screen Visual Editing mode.

To get started with Visual Editing, [share the docs](https://www.datocms.com/docs/general-concepts/visual-editing.md) with your devs and get them to implement it!

[

(Image content)

Next episode

Working with the DatoCMS MCP

](https://www.datocms.com/user-guides/content-management/working-with-the-datocms-mcp.md)

---

# Intro to the Schema Builder

Source [user-guides]: https://www.datocms.com/user-guides/the-basics/intro-to-the-schema-builder.md

3m 04s

The Schema Builder is among the most important parts of the CMS for the devs, or whoever is in charge of Content Modeling for your project, so we won't be diving into too many of the details from a technical standpoint.

However, since Content Modeling is a fundamentally crucial concept to understand when moving to Headless CMS, we will be dedicating a whole chapter on this section of the CMS for you, as an editor, to gain understanding and context into how decisions made here impact your overall content creation experience.

To learn more about how a Headless CMS works and how Content Modeling sets the foundation for all the work you'll be doing in DatoCMS, check out our [Academy](https://datocms.com/academy) where we cover this as well as some other core concepts.

A quick TLDR on this though, is to think of your schema as a LEGO project, with each of the fields being an individual LEGO brick. You can build what you want with any combination of bricks, and use and reuse the same bricks across multiple LEGO creations. Similarly, fields build out blocks and models in DatoCMS, which are the "templates" or "structures" of your content!

[

(Image content)

Next episode

Intro to the Asset Area

](https://www.datocms.com/user-guides/the-basics/intro-to-the-asset-area.md)

---

# Images and Image Optimization

Source [user-guides]: https://www.datocms.com/user-guides/media-management/images-and-image-optimization.md

6m 20s

Images are a crucial part of working on projects, and Dato's media area offers some particularly useful asset management capabilities, particularly from an optimization and SEO standpoint.

Each image uploaded has certain metadata management happening under the hood, as well as giving you options to enrich them further:

-   Files can be uploaded in any format - jpg, png, gif, webp, whatever, AND can be transformed when served to the frontend via the powerful image optimization features offered by our API partner, imgix.
-   You have complete control on renaming files for SEO purposes, as well as adding in new titles, alt-text, and custom fields for accessibility.
    
-   👆and yet, you can set those per locale, not just per file!
-   We'll also bring in all the fun metadata from the file's EXIF, like any credits, lens details, copyrights, MME type, etc.
    
-   In the background we'll extract the dominant color palette for when and if you need to query those for any frontend magic, and we'll also let you select the focal point in the CMS to make sure the images look crisp on all devices.
-   While we'd auto-tag images with anything that shows up, you can also manually override or add more tags for content purposes
    
-   On the dev side, when serving these images, your team can apply powerful transformations to enhance, change quality, change format, set dimensions, crop, and a ton of other stuff ([check the docs for deets](https://docs.imgix.com/apis/rendering)), so as editors you really don't need to bother manually optimizing things. Got a 300MB AVIF photograph? [Just upload it](https://www.datocms.com/blog/making-media-optimization-a-breeze-with-datocms.md#see-it-in-action). The frontend will handle serving it as an optimized 50kb webp.
-   Uploaded something and not happy with it? Don't upload another one to replace everywhere in your content, just `Replace Asset` from the media area, and we'll make sure it's updated everywhere (note: The asset URL *will* change though, so something for your devs to keep in mind!)
    
-   Dato also offers some basic image editing options in the media area for simple things like rotation, adding filters, markup, and crops, in case you want to get a little creative after dumping images in.
    

Finally, all DatoCMS projects come with some optimizations on by default. Under your Project Settings you can disable or edit these to apply them across the entire project.

[

(Image content)

Next episode

Videos and Video Optimizations

](https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md)

---

# Intro to Models in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-models-in-datocms.md

2m 56s

In this video we go over the basics of Content Modeling, and how a project's foundation is based on the way it's modeled.

For an in-depth understanding of Content Modeling, we'd recommend checking out [our Academy post](https://www.datocms.com/academy/headless-cms/how-a-headless-cms-works.md#content-modeling) on the topic.

Models are the basis of creating content in DatoCMS - where each model corresponds to the template of a singular content type: i.e., blog posts, landing pages, banners, etc.

Models are composed of several field types - where each field serves a specific purpose for accepting data (such as the title, slug, featured image, etc.)

Models in DatoCMS can be built using the following field types:

-   Text,
-   Modular Content (blocks, covered in the next video),
    
-   Media,
-   Date & Time,
    
-   Number,
-   Boolean,
    
-   Location,
-   Color,
    
-   SEO,
-   Links, and
    
-   JSON.
    

A good analogy to think of the relationship between models and fields in content modeling is to imagine a big pile of LEGO bricks. Each piece, or "field," can be anything you need—a text input, an image, or a date picker. You combine these together to build different structures.

For example, when you're putting together a model for a blog post, you might choose a combination of text, image, and date fields to accept titles, publishing dates, featured images, and content. It's like choosing the right LEGO pieces to build a flashy castle.

On the other hand, an author model might be similar to building a LEGO house - using text and image fields for things like the author's name, title, and avatar.

So, each time you set up a model—whether it’s for a landing page, a blog post, or anything else—you’re deciding how to fit all these different fields together to best display your content, just like picking the perfect LEGO set to build something.

DatoCMS has a further advanced content modelling capability using the unique Modular Content field - one that accepts blocks like CTAs, newsletter signups, and testimonials into your model, something we'll cover in the next video.

[

(Image content)

Next episode

Intro to Blocks in DatoCMS

](https://www.datocms.com/user-guides/content-modeling/intro-to-blocks-in-datocms.md)

---

# Intro to Blocks in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-blocks-in-datocms.md

3m 25s

Blocks are a concept unique to DatoCMS and are the foundation behind powerful flagship features such as [Modular Content](https://www.datocms.com/docs/content-modelling/modular-content.md) and [Structured Text](https://www.datocms.com/docs/content-modelling/structured-text.md).

In a nutshell, blocks allow you to define **complex and repeatable structures that can be embedded inside records**. Think of components like a hero, CTAs, logoreels, and case study cards - they'd typically be complex structures that you'd want to either repeat across models, or embed into certain content records. Several landing pages can have variations of a logoreel, or several blog posts can have embedded newsletter signups or testimonials.

Rather than having to create the same subset of fields into those models which may or may not be used across all use-cases, blocks allow you to define those structures beforehand, and simply embed them into content records if and when you choose to!

On the content editing side, modular content also unlocks the ability to generate "drag-and-drop" experiences on the CMS, giving editors the power to build powerful templates with the familiarity and simplicity of a page builder.

[

(Image content)

Next episode

Intro to Fields in DatoCMS

](https://www.datocms.com/user-guides/content-modeling/intro-to-fields-in-datocms.md)

---

# Intro to the Media (Assets) Field

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-the-media-assets-field.md

5m 10s

Similar to the Text fields, you also have a few options when it comes to media fields - more specifically, the ability to set the field as a Single Asset field (i.e. accept one file), an Asset Gallery (accept multiple), or an External Video field (i.e. accept a URL to a video hosted on Facebook, Vimeo, or YouTube).

Asset Management in DatoCMS is powered by imgix and Mux, leading APIs that give DatoCMS users their powerful capabilities, optimizations, and global delivery.

Within the Asset field, you can give your editors certain restrictions on which filetypes/formats they can add into content, what specific sizes, dimensions, or aspect ratios they can add, and whether or not alt-texts and titles are required.

Note: While you can restrict these on the model level, we highly recommend [checking out our docs](https://www.datocms.com/docs/content-delivery-api/images-and-videos.md) to learn more about transformations and optimizations offered through our Images and Video APIs that would potentially make it more scalable to your workflow.

[

(Image content)

Next episode

Intro to the Link Field

](https://www.datocms.com/user-guides/content-modeling/intro-to-the-link-field.md)

---

# Intro to the SEO Fields

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-the-seo-fields.md

4m 07s

SEO fields are another extremely important field type in DatoCMS. It's worth noting that not all Headless CMS would offer an SEO field, since many have no control and/or context into the actual web-facing aspects of the project you're building.

However, since we offer our own [SEO packages](https://www.datocms.com/docs/content-modelling/seo-fields.md) for developers to easily query for all the OG and metadata from your content, we're able to make things simpler for you.

On the SEO side, aside from any plugins (there's a few fun ones like the SEO readability analysis), DatoCMS offers a `slug` and a `SEO and Social` field.

The slug is a nifty text field that automatically generates slugs for your pages and posts, usually inheriting values from another field like a text field for the title. This comes with specific validations to only accept slug formatting (i.e. no spaces etc.) and to be unique.

The SEO and Social field, however, is a little more of a familiar zone for many of you coming from Web CMS. It's where you can manually override your OG tags like `og:title`, `og:description`, and `og:image`, as well as get your social sharing previews for popular platforms like Facebook and LinkedIn. Well, Google SERPs, Twitter, Facebook, LinkedIn, Slack, Whatsapp, and Telegram, to be more specific.

This is also the field where you can set the option for editors to choose whether or not they want to add in a `_noindex` tag to the content they're working on, discouraging SERPs from indexing those pages when you generate new sitemaps.

[

(Image content)

Next episode

Intro to the Modular Content Field

](https://www.datocms.com/user-guides/content-modeling/intro-to-the-modular-content-field.md)

---

# Intro to the Modular Content Field

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-the-modular-content-field.md

7m 53s

Another unique feature to DatoCMS, the Modular Content field is used to define a dynamic area for richer page layouts, available in two flavours - a Single Block and Multiple Blocks.

For example, in a landing page, defining a Modular Content field allows the writer to choose between adding a text section, a carousel, or a CTA, depending on which blocks have been created. This gives the writer the freedom to compose a landing page by alternating and ordering as many of these choices as they want/need.

You can use Modular content for defining dynamic layouts in any of your models where you want to give the content writers the choice between different template options within some guidelines.

For more details on making block editing feel "native" to the model itself, check out the release and details on the [Frameless Presentation](https://www.datocms.com/blog/expanding-modular-content-with-single-block-and-frameless-mode.md) mode.

[

(Image content)

Next episode

Intro to Other Fields (Address, JSON, Number, etc.)

](https://www.datocms.com/user-guides/content-modeling/intro-to-other-fields.md)

---

# Understanding SEO in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/content-management/understanding-seo-in-datocms.md

12m 51s

This video switches context a bit, between giving a high level understanding of how important the technical implementation of the website is to follow best practices when it comes to SEO, as well as a little look into how DatoCMS enables devs to implement SEO well.

We then move on to understanding how DatoCMS offers some in-built optimization settings for asset SEO and how our CDN works for that, before getting into the CMS to show certain best practices for things like favicons, Open Graph tags, and content creation.

We'll also take a quick cheeky look into Asset SEO on a per-image basis, since DatoCMS offers fields for titles, altText, and other metadata within the Media Area.

While it's definitely best to follow best-practices on on-page and off-page SEO on an ongoing basis and to maintain content hygiene, Dato makes some things simpler for us as content editors.

Note: While DatoCMS does offer a blanket `noIndex` option that applies to the whole project, it's only recommended to use this while the project is in development mode, and not as a toggle for ongoing sitemap refactors. For individual content records, it's best to handle `_noIndex` per record.

For a higher-level conceptual understanding of [Headless CMS and SEO, check out our Academy post on the topic](https://www.datocms.com/academy/headless-cms/headless-cms-and-seo.md).

[

(Image content)

Next episode

Content Records, Publishing, Scheduling, and Versioning

](https://www.datocms.com/user-guides/content-management/content-records-publishing-scheduling-and-versioning.md)

---

# Videos and Video Optimizations

Source [user-guides]: https://www.datocms.com/user-guides/media-management/videos-and-video-optimizations.md

3m 14s

Similar to Imgix for images, we work with Mux for videos, and they're among the best Video APIs out there!

Videos are treated *sliiiightly* differently than images, but your overall experience as an editor should remain unchanged:

-   Videos can be uploaded in any format - `mp4`, `mov`, `flv`, `mkv`, whatever.
-   Mux also offers a lovely experience with their streaming API, so you can use DatoCMS to livestream video rather than just uploading static video files.
    
-   Similar to images, we extract all available metadata for you to work with, including the dimensions, durations, frame rates, and any copyrights and tags that can be extracted.
-   You also have the option to replace names, titles, and alt text fields per file and per locale, just like with images.
    
-   We'll also bring in all the fun metadata from the file's EXIF, like any credits, lens details, copyrights, MIME type, etc.
-   The 🪄 magic 🪄, however, really comes down to how Mux handles them after you've uploaded videos. While you can definitely manually select things like quality of delivery and resolution (all the way from 240p to 4K), you can also just forget about it. Mux's delivery can be optimized to serve people the best format and quality for their bandwidth and device, making videos extremely efficient to serve.
    

## How to serve optimized videos

When you're ready to put the videos on your website, have your developer check out our detailed [video optimization guide](https://www.datocms.com/docs/streaming-videos/how-to-stream-videos-efficiently.md) for implementation details!

[

(Image content)

Next episode

Media SEO in DatoCMS

](https://www.datocms.com/user-guides/media-management/media-seo-in-datocms.md)

---

# Building Pages and Deep Dive into Modular Content

Source [user-guides]: https://www.datocms.com/user-guides/content-management/building-pages-and-deep-dive-into-modular-content.md

17m 56s

This is a heavy and packed video, so grab a coffee! We're going to be walking through 4 common approaches that we ourselves take at DatoCMS, when working with the CMS and building pages:

-   A low-touch CMS page where most of the content is handled via the frontend and code, such as our blog index on `/blog,`
-   An index page for `/case-studies` where we link the page to individual case studies, and drag and drop them based on how we want them to appear on the website,
    
-   A resources page for `/resources` that looks at the option of ordered records, where editors can arrange all the records of a model for when we don't want to have a seperate model to link to, and
-   A homepage, using our marketing website starter in context, to showcase the power of a visual drag-and-drop experience using only modular content and a plugin for live previews.
    

And, and, and, as of Q4, 2024, we've made it EVENNN easier to work with Modular Content. If you're a heavy user of blocks, you'll notice some really nice UX improvements thanks to **bulk actions**. You can now easily select multiple Modular Content items and perform actions all at once from a brand new action bar.

(Video content)

Each Modular Content row now includes a checkbox for easy selection and you can perform bulk actions such as:

-   Select All / Invert Selection
-   Expand / Collapse selected blocks
    
-   Copy multiple blocks
-   Delete selected items
    

(Video content)

The contextual submenu also makes managing blocks a bit easier, with an improved flow to:

-   Copy & Paste
-   Duplicate
    
-   Move
-   Delete, and
    
-   Add Blocks
    

You can try the full potential of Modular Content yourself by spinning up a new project using either our [marketing](https://www.datocms.com/marketplace/starters/marketing-website.md) or [eCommerce](https://www.datocms.com/marketplace/starters/ecommerce-website.md) starters! Give them a spin.

[

(Image content)

Next episode

Visual Editing in DatoCMS

](https://www.datocms.com/user-guides/content-management/visual-editing-in-datocms.md)

---

# Intro to the Plugin Ecosystem

Source [user-guides]: https://www.datocms.com/user-guides/the-basics/intro-to-the-plugin-ecosystem.md

8m 19s

Since a Headless CMS is essentially just a hosted API for all your content, the possibilities of what you can build with one are pretty endless - websites, dishwasher screen UIs, OOH displays, life-size livestream of a Catan game - anything.

This also means that a Headless CMS should allow you to unlock crazier use-cases that go beyond slug generation and SEO to things like AI tagging, product picking, or metadata stitching (and anything else you can think of).

If we provided every possibility natively through the core product, RIP bandwidth and brains trying to create and digest all the possibilities. So we have plugins, which are a fundamental feature to extensibility and integrations within DatoCMS, allowing install to use and/or create new possibilities using simple React apps.

Our [Plugin ecosystem](https://www.datocms.com/marketplace/plugins.md) lists out publicly available plugins for common use-cases like Content Calendars, Web Previews, and Product Pickers, but you can also create private plugins for very specific things you'd need.

Need a 3D sprite picker for an in-game event? Make a plugin. Need a voice generator to read you your content entries with some lo-fi background music while you're writing them? Make a plugin. Need AI to fill out all your content for you to just hit `publish`? Make a plugin. You get the idea.

Plugins are available across DatoCMS, letting you customize your views via sidebar plugins (like [Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md)), field plugins (like a Star Rating picker), or full-screen plugins (like a [Content Calendar](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-content-calendar.md)).

[

(Image content)

Next episode

Intro to Models in DatoCMS

](https://www.datocms.com/user-guides/content-modeling/intro-to-models-in-datocms.md)

---

# Deep Dive into Structured Text in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/content-management/deep-dive-into-structured-text-in-datocms.md

9m 37s

This video is all about the magic🪄 of the Structured Text field! We designed the Structured Text editor to offer one of the best writing experiences on the market. It supports Slash commands, Markdown shortcuts, and full-screen focus mode. Here's a quick video of it:

(Video content)

As we work together on trying out all the options this field gives us, we're going to see how slash commands, markdown, formatting options, and/or typing in "focus mode" give us a really smooth and immersive content creation experience.

Many of us are used to creating content in other tools and then just pasting them over to the CMS, but that's a thing of the past when working with this field!

As we create content, embed media, and do some formatting, we'll also see how we can expand this model to accept new media types (like a PDF) and blocks, and see how certain validations restrict us from creating content we're not supposed to (such as going beyond H2).

The Structured Text field also let's you extend the editor with custom plugins within the field itself, but I'm not nearly technical enough to make that on the fly - so rather than making myself look like a fool, [check out the docs to explore the possibilities](https://www.datocms.com/docs/plugin-sdk/structured-text-customizations.md) with your devs!

[

(Image content)

Next episode

Understanding SEO in DatoCMS

](https://www.datocms.com/user-guides/content-management/understanding-seo-in-datocms.md)

---

# Understanding the Media Area in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/media-management/understanding-the-media-area-in-datocms.md

7m 36s

The media area in DatoCMS is where you'd manage all your images, videos, audio files, and other documents your project might need.

The media view comes with its own customization options to view your files as a list or grid, add some custom filtering options, and similar to the content area, create saved filters for you and your team. There's also some nice optimization presets and asset collection options to get familiar with.

One nifty feature in the media area though, is that you can preview certain files right there, without having to launch the asset. For instance, you can play audio files in the grid to get a quick preview. If you're a Mac user and familiar with previewing files in Finder without opening QuickTime or another app, this will feel just like that!

[

(Image content)

Next episode

Images and Image Optimization

](https://www.datocms.com/user-guides/media-management/images-and-image-optimization.md)

---

# Intro to the Asset Area

Source [user-guides]: https://www.datocms.com/user-guides/the-basics/intro-to-the-asset-area.md

2m 27s

Images and videos form the foundations of any rich digital experiences. The DatoCMS media area comes with support for most formats of pics, vids, audio, and files - giving you a lot of control over what your projects need to show!

With certain media optimization settings available out of the box, SEO options, and much more, the media area is an important part of the CMS to master as you proceed to manage your content!

[

(Image content)

Next episode

Intro to Settings & Configurations

](https://www.datocms.com/user-guides/the-basics/intro-to-settings-configurations.md)

---

# Content Records, Publishing, Scheduling, and Versioning

Source [user-guides]: https://www.datocms.com/user-guides/content-management/content-records-publishing-scheduling-and-versioning.md

6m 04s

This one's a bit of a meta video, where we don't create anything new, but just look into common workflows and information available to us on the content record info sidebar.

-   All records give us some useful record metadata on things like who's created content, when it was created, and when it was edited. While these fields have importance on the frontend for querying content chronologically (like with blog posts) for example, they also give us editors the ability to sort our content views as we like, giving us some customization.
-   On the Publishing section we have the ability to look into who published content and when, and depending on which workflow stages you have available to you in your project, you can move things around from a published stage to another. If you have localizations enabled, this is also where you can select to unpublish the entire record, or only do so specifically for a certian locale.
    
-   Then we have the scheduling section which gives us the ability to schedule publishing and unpublishing actions for future days, per locale. A great feature for time-bound records that have to do with things like offers or promotions, scheduling gives us the peace of mind to know that the CMS will handle things in the future without us needing to remember to intervene!
-   Following that is the Links area, where we can see which other records this record is connected to. For example, when we take a look at one of the authors, we can see all blog posts they're related to. Another nifty view for knowing what other content would get affected if we make changes to and/or delete the record we're looking at.
    
-   Last up we've got the versions section. This is where you get a chronological view of all the changes this record has had (as long as they were saved and/or published), to get a side-by-side comparison of changes. You can revert content to a previous version if you decide that the most recent version isn't to your liking, for example. How many versions you have available to you depends on which DatoCMS plan you're on.
    

Note: The sidebar can change depending on your usage of plugins. In one of the following videos we cover how the side-by-side preview changes how the sidebar looks and behaves.

[

(Image content)

Next episode

Building Pages and Deep Dive into Modular Content

](https://www.datocms.com/user-guides/content-management/building-pages-and-deep-dive-into-modular-content.md)

---

# Working Together - Creating Our First Blog Post

Source [user-guides]: https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-blog-post.md

8m 48s

In this video we're going to tackle creating blog posts! However, we're going to be splitting the flow into two videos - in this one, we'll zip through the whole flow of what creating a blog post would look like, and in the next video we'll do a deep dive into the Structured Text field, to get a feel of all the features and capabilities that field gives us.

When creating this post, we're also going to see how having an asset field as part of the model, unlike with the case study, changes our previews when looking at SERPs and social platforms.

Since the next video will cover the Structured Text field, in this one we'll take a look at other workflows we're used to working with - like copy-pasting content over from popular platforms like Google Docs or Notion - to see how content and its formatting comes through into DatoCMS.

We'll take a small sneak peek into slash commands regardless, as we enrich our post with images, before moving on to looking at how localizations work in DatoCMS as we localize this blog post into Italian as well.

[

(Image content)

Next episode

Deep Dive into Structured Text in DatoCMS

](https://www.datocms.com/user-guides/content-management/deep-dive-into-structured-text-in-datocms.md)

---

# Working Together - Creating Our First Case Study

Source [user-guides]: https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-case-study.md

10m 41s

Now that we've got a hang of creating records and enriching content with blocks, let's go ahead and create our first full record - a case study.

This model tackles using quite a few fields - we've got text, modular content, links, and SEO fields.

So to prep our case study content, let's first create a new person, this time, selecting them as our customer.

When enriching this person with the testimonial block, let's also attach a video that we've got from the case study interview - however, be wary of any gotcha moments as we try to upload an `.flv` to a field where we'd kept a validation to only accept `.mp4`. The "wrong" file will be uploaded, processed, and available in the media area, but we won't be able to save this testimonial with that file since it doesn't pass the validation.

Once we've got the person ready, let's go ahead and actually create our case study. Now we can add in all the content for the fields, such as the title, slug, and description, before making use of the link field to associate the customer with this case study.

The actual content for the case itself is going to be a combination of modular content blocks, since we'd said we wanted a relatively flexible layout in terms of how content is arranged, but we didn't want to deviate from the acceptable content formats themselves.

So we'll drop in a few content blocks, statistics, and FAQs to complete our first case study and publish it.

This will also be the first time we encounter the SEO fields in practice, giving us a preview into how our case study would look on SERPs, as well as on some social and chat platforms where we might commonly want to share these links.

[

(Image content)

Next episode

Working Together - Creating Our First Blog Post

](https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-blog-post.md)

---

# Working Together - Enriching Content With Blocks

Source [user-guides]: https://www.datocms.com/user-guides/content-management/working-together-enriching-content-with-blocks.md

12m 24s

In this video we're going to achieve 4 things together:

-   First, we'll improve the Person model to associate them with a "category" of an author, a customer, or "other". This is because we'd said we wanted the person model to be relatively "use-case agnostic" and be used fluidly across the project, rather than having different models for authors and for customers. This is by no means the "best" or the "right" approach, but just one of the many approaches you can take for models that need to be associated with others for various reasons.
-   Next up we'll fix my mistakes evolve our schema to remove the asset field from the contact info block, and move it into the person model itself. This is for 2 reasons:
    
    -   First, people have an avatar even if they have no contact info associated with them, and
        
    -   Secondly, by having an asset field as part of the model itself, we give ourselves a prettier UI when previewing all the content on the content view, since we can't bring out the content from blocks.
        
-   Third, we're going to create some new people, they'll all be of the author category, and
-   Lastly, we'll start enriching the people we've created with some contact info to look at how creating content in blocks looks like.
    

[

(Image content)

Next episode

Working Together - Creating Our First Case Study

](https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-case-study.md)

---

# Working Together - Creating Our First Record

Source [user-guides]: https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-record.md

4m 22s

In this video we're going to create our very first model together - by creating our first person in the `people` model. Since they're the ones covering the video, we'll quickly create a person for Alessio and for Ronak, and get a quick look into which workflow stages they exist in - `DRAFT` and `PUBLISHED`.

We'll also make our first change to improve our schema, by adding in a `REQUIRED` validation on the person's name, to avoid saving blank records with just a record ID.

[

(Image content)

Next episode

Working Together - Enriching Content With Blocks

](https://www.datocms.com/user-guides/content-management/working-together-enriching-content-with-blocks.md)

---

# Understanding the Content Area

Source [user-guides]: https://www.datocms.com/user-guides/content-management/understanding-the-content-area.md

4m 23s

The content area is where you'd be doing most, if not all, of your day to day work in DatoCMS if you're an editor or on the content team.

In this video we go through a quick recap of what we did in Chapter 2, to remember the models and blocks we made, which we'll be editing throughout this chapter.

The first thing you'd notice in the content area is that all the models we'd created - the people, blog posts, pages, etc., are neatly tucked away in the sidebar for us to start creating content with - but the blocks we'd created, such as the contact info and testimonials, don't show up. This is because blocks are inherently "embedded", or a part of models, and cannot be treated as standalone content records.

You also cannot filter the content view by blocks which are part of the content, which is why it's important to plan ahead and know which pieces of content are most "critical" or "frequent" to your project, and have them as individual fields or models.

You can, however, customize your content view to best suit your workflows! With powerful column configurations, filters, and search, you can tweak your content view to look and feel as you like, and save those filters as custom views for just you, or for your team.

[

(Image content)

Next episode

Working Together - Creating Our First Record

](https://www.datocms.com/user-guides/content-management/working-together-creating-our-first-record.md)

---

# Schema Organization & Wrapping Up

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/schema-organization-wrapping-up.md

2m 39s

To wrap things up in this chapter, let's just spend a few minutes going over some best practices to keep your schema builder clean, by making use of some icon-based sorting and model grouping, making it easier to navigate your models and blocks when working on particularly large projects.

[

(Image content)

Next episode

Understanding the Content Area

](https://www.datocms.com/user-guides/content-management/understanding-the-content-area.md)

---

# Bonus Content - Lo-fi Break for Models

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/bonus-content-lo-fi-break-for-models.md

1m 35s

Nothing much going on here - sit back and enjoy some royalty free lo-fi music while we build out some more models that're going to be used in upcoming videos.

Expanding on the previous video, we'll build some more models to use in the background, more specifically:

-   **Blog Index** Page to query blog posts on,
-   **Case Study Index** Page to link Case Studies to, and
    
-   **Homepage**, to create a drag-and-drop page builder with some restrictions in place
    

[

(Image content)

Next episode

Schema Organization & Wrapping Up

](https://www.datocms.com/user-guides/content-modeling/schema-organization-wrapping-up.md)

---

# Working Together - Let's Build Models

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/working-together-let-s-build-models.md

14m 56s

In this video we're going to be working together in creating our first "real" models for the project moving forward.

During this video, let's go ahead and build out these:

-   A **person**: A use-case agnostic model for a person, who could either be an author, a customer, or any other person. In many use-cases you wouldn't model "people" like this you'd be a bit more specific, but this example aims to highlight how you can filter people and relate them to other models appropriately to stretch the imagination a bit with "reusable" content.
    
    -   **Single-line String** Field, for the name of the person,
        
    -   **Single-line String** Field, for the title of the person
        
    -   **Single-line String** Field, for the company of the person, and
        
    -   **Modular Content** Field, to embed the **Contact Info** and the **Testimonial** blocks, as they'd be used to enrich the person model
        
-   A Blog Post: A simple layout to focus on the content creation experience, modelled similarly to the DatoCMS blog posts.
    
    -   **Asset** Field, for a Featured Image that most blog posts would have,
        
    -   **Single-line String** Field, for the title of the blog post,
        
    -   **Structured Text** Field, for the description of the blog post,
        
    -   **Structured Text Field with embedded Blocks**, for the main content of the post, accepting blocks for images, videos, newsletter signups, testimonials, and statistics,
        
    -   **Slug** Field, to generate the slug of the post based on the title, with a `unique` validation in place,
        
    -   **SEO and Socials** Field, to see our previews and override any fields if necessary, and
        
    -   **Link** Field, to associate the blog post to an author via the person model
        
-   A **Case Study** template focusing on Modular Content to allow the "page building" experience within certain guidelines.
    
    -   **Single-line String** Field, for the title of the case study,
        
    -   **Multiple Paragraph** Field, for the description of the case study,
        
    -   **Modular Content** Field, for the actual content of the case study, to create a drag-and-drop layout using the Content Block, Statistic, FAQ, Testimonial, Image, and Video blocks,
        
    -   **Link** Field, to relate the case study to a customer via the person model,
        
    -   **Slug** Field, to generate the slug of the case study based on the title, with a `unique` validation in place, and
        
    -   **SEO and Socials** Field, to see our previews and override any fields if necessary.
        
-   An **About** page, showcasing the "single instance model" feature, ensuring that only one about page is created and no more:
    
    -   **Single-line String** Field, for the title of the page,
        
    -   **Modular Content** Field, for the content of the page, and
        
    -   **Slug** Field, to generate the slug of the case study based on the title, with a `unique` validation in place.
        

[

(Image content)

Next episode

Bonus Content - Lo-fi Break for Models

](https://www.datocms.com/user-guides/content-modeling/bonus-content-lo-fi-break-for-models.md)

---

# Bonus Content - Lo-fi Break for Blocks

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/bonus-content-lo-fi-break-for-blocks.md

2m 16s

Nothing much going on here - sit back and enjoy some royalty free lo-fi music while we build out some more blocks that're going to be used in upcoming videos.

Expanding on the previous video, we'll build some more blocks to use in the background, more specifically:

-   **Image** block to embed into structured text and landing pages,
-   **Video** block to embed into structured text and landing pages,
    
-   **CTA** to accept buttons,
-   **Newsletter** **Signup** including signup text, an email address placeholder, and a CTA,
    
-   **Hero** to use on landing pages with a featured image, headline, description, and CTA,
-   **FAQ** with a question and answer,
    
-   **Content** **Block** to showcase text and images side by side in a single row, and
-   **Statistic** accepting a single stat with a text field for a description.
    

[

(Image content)

Next episode

Working Together - Let's Build Models

](https://www.datocms.com/user-guides/content-modeling/working-together-let-s-build-models.md)

---

# Working Together - Let's Build Blocks!

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/working-together-let-s-build-blocks.md

11m 38s

In this video we're going to be working together in creating our first "real" blocks for the project moving forward. We've created some arbitrary models and blocks to illustrate how the schema builder works, so now let's trash those and build out some more realistic ones that we can use moving forward, keeping in mind that our project is intended to be a B2B SaaS website.

During this video, let's go ahead and build out these:

-   A **logoreel**: A simple way to flex our customers on our website, commonly seen on most landing pages and homepages, for which we'll use:
    
    -   **Asset** Field, as a gallery, to attach logos of our customers,
        
    -   **Single-line String** Field, as a tagline, to add a witty sentence giving context about those logos, and
        
    -   **Color** Field, as a black and white toggle, to dictate a dark mode and a light mode to our logoree.
        
-   A **testimonial**: A barebones structure (not attributed to a person, as we'll embed those to the person model in the future) to accept quotes and videos from our customers to showcase our successes, for which we'll use:
    
    -   **Asset** Field, as a video upload field with a validation to only accept mp4 files, where we can add videos, and
        
    -   **Structured Text** Field, to add in well-structured and formatted quotes from our customers
        
-   A **contact info**: A comprehensive block to attach to people and places with common contact information, for which we'll use:
    
    -   **Asset** Field, to upload an image of the person and/or location this address will be used with,
        
    -   **Single-line String** Field, to use as a Reference, in case we want something internal to search by in the future,
        
    -   **Geolocation** Field, to add a street address via the Google Maps API, which will provide us latitude-longitude coordinates as well,
        
    -   **Single-line String** Field, for an email, with an email validation added,
        
    -   **Single-line String** Field, for a Twitter/X URL, with an URL validation added,
        
    -   **Integer Number** Field, for phone numbers, accepting only whole numbers, and
        
    -   **Structured** **Text** Field, for a bio, allowing us to add formatted content to the description of the person or the place.
        

[

(Image content)

Next episode

Bonus Content - Lo-fi Break for Blocks

](https://www.datocms.com/user-guides/content-modeling/bonus-content-lo-fi-break-for-blocks.md)

---

# Intro to Other Fields (Address, JSON, Number, etc.)

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-other-fields.md

5m 32s

This video is a quick walkthrough some of the other field types that each have specific use-cases, but may not be as commonly used as the ones we've covered in detail. To get some context on them, these are:

-   **Date and Time**: A timestamp value for storing dates and times (i.e. an event start, office opening hours).
-   **Number: Integers and/or Floating Numbers, f**or storing integer SKUs, quantities, prices, etc.
    
-   **Boolean:** For storing values that have two states, e.g., yes or no, true or false etc.
-   **Location:** Coordinate values and street addresses for storing the latitude and longitude of a physical location.
    
-   **Color:** For storing colors (with or without alpha channel).
-   **JSON:** For storing JSON objects and snippets.
    

[

(Image content)

Next episode

Working Together - Let's Build Blocks!

](https://www.datocms.com/user-guides/content-modeling/working-together-let-s-build-blocks.md)

---

# Intro to the Link Field

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-the-link-field.md

4m 38s

Link fields are another nifty solution to show that different content types have a relation to one another, without needing to create duplicate fields to repeat content over and over again.

In the video, we take on the example of how blog posts are related to authors writing the blog posts, each being their own individual model. But why take this approach? Blog posts have authors, so technically, you could always extend the blog post model to have an author name, author avatar, and author bio. That would work.

But then the same author's written multiple posts. Why fill out repetitive information over and over again? This is where links come in!

We simply need a single author, let's call them Veel. Veel's written multiple blog posts, so she can be linked to them every time a new post is created, no repeating content every time!

The link field also comes in two flavors:

**Single Link**: The ability to link to just one other model, i.e, a blog post to an author, or

**Multiple Links**: The ability to link to a collection of eternal records, i.e., a blog post to a group of authors.

On the Validations side, there isn't much new here, save for the inclusion of you having to select *which* model(s) can be linked to via the model you're creating.

On the presentation side, however, you can select whether editors see a compact view (i.e. pills with the title of the field related), or an expanded view (tabs with a little more information.)

[

(Image content)

Next episode

Intro to the SEO Fields

](https://www.datocms.com/user-guides/content-modeling/intro-to-the-seo-fields.md)

---

# Intro to String (Text) Fields

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-string-text-fields.md

8m 57s

The text field, also commonly referred to as the string field, is perhaps among the most commonly used fields within the CMS. DatoCMS offers you 3 flavors of text fields:

-   **Single-line String**: A simple text input field for short forms of plain text, such as names, headlines, and button labels.
-   **Multi-paragraph Text**: A larger text editor for writing content in Markdown, HTML, or Plain text, commonly used for small content blocks or descriptions.
    
-   **Structured Text**: A unique and powerful DatoCMS text editor with powerful editing capabilities like slash commands, embeddable blocks, and focus mode, allowing you to focus on creating content with a great UX, and the ability to embed images, videos, maps, and anything else into the field.
    

Text fields also come with some pretty nifty validations and presentation options.

Aside from marking them as required or unique, the single-line string in particular offers you built-in validations to only accept specific character counts, and patterns, such as URLs, emails, and other RegEx. The single-line string can also be set to present itself to editors as a text input, radio group, or an input selector, aside from the ability to only accept specific inputs.

While the multi-paragraph field isn't intended for things like URLs and Email addresses, it does still offer the same validations to be set as `required` or match patterns. However, since this field type can be set to accept HTML, there is an added validation to prevent the use of dangerous HTML attributes. Given the extended use-cases for this field, the presentation tab allows to select between a plain text editor, a Markdown editor, or an HTML one, as well as provide some formatting toolbar options on which headings, style options, and link options editors can have.

And finally, the structured text field has some unique new aspects to it. Since this field allows you to both, embed blocks, and link to other models, you have the flexibility to select which blocks can be embedded and which inline records the field can accept. For example, an editor can both, link to an author model, as well as embed a testimonial block into the editor, depending on how much flexibility you give editors. On the presentation tab, you can select what editors can do - between formatting options, allowed heading levels, allowed formattings, and how links and blocks would be treated in the editor.

[

(Image content)

Next episode

Intro to the Media (Assets) Field

](https://www.datocms.com/user-guides/content-modeling/intro-to-the-media-assets-field.md)

---

# Intro to Fields in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/content-modeling/intro-to-fields-in-datocms.md

1m 41s

In the video for models we'd illustrated how field types are individual LEGO bricks that come together to create models.

In the following videos we'll be covering all of Dato's field types in more detail, however, for this one we're just going to zip through what all the field types share in common.

When configuring fields, they all offer you 4 tabs:

-   The **Settings**, where you name your fields and control their localization and API IDs,
-   **Validations**, where you set restrictions or rules in place for editors to abide by (example, all records must be `unique`, or localizations are `required`),
    
-   **Default Value**, to ensure that all new content records are created with a default and/or fallback value (⚠️ be wary of using this when setting fields to `unique`), and
-   **Presentation**, where you define how this field appears to an editor on the Content Area UI
    

While some specifics for each tab might change based on the field type you're configuring, DatoCMS offers you certain levels of flexibility when building out your models for editors to create content with.

[

(Image content)

Next episode

Intro to String (Text) Fields

](https://www.datocms.com/user-guides/content-modeling/intro-to-string-text-fields.md)

---

# Media SEO in DatoCMS

Source [user-guides]: https://www.datocms.com/user-guides/media-management/media-seo-in-datocms.md

3m 21s

We've touched upon this a few times here and there, but here's a recap into how DatoCMS makes your Media SEO management a bit nicer:

-   Automatic image optimization using presets from imgix parameters that apply to the entire project,
-   Manual control over file titles, alt-text, and description per locale and file,
    
-   Automatic image tagging and metadata management for easier accessibility and licensing with the option to override certain fields like tags,
-   Custom fields for any further data enrichment that needs to happen per file, and
    
-   Powerful optimizations to serve the best format and size of asset per device ensuring technical performance isnt' sacrificed.

---

# Intro to Settings & Configurations

Source [user-guides]: https://www.datocms.com/user-guides/the-basics/intro-to-settings-configurations.md

5m 00s

We all love a bit of control over our projects, but in many cases as editors and content creators we might not have access to all the options that DatoCMS offers.

So here's a sneak peek into the settings and configs within Dato for your projects, for you to bug your devs for any time you need something changed. If they say it can't be done, now you know it can be 💁

## Project Settings

Global Properties are where you'd force the use of sandbox environments and set up 2FA. Dato also offers Asset CDN Settings to give you more control over how images and videos are served, what optimization options are available, and what presets you can work with across the project.

If you're working on the project with multiple people, this is where you can invite collaborators as well as define specific roles such as Admin, Editor, French Translator, Klingon Translator, Slackbot Configurator, Press Approver, and whatever else, depending on your plan.

A few other dev-specific aspects are also controlled from here, such as webhooks, build triggers, environments, and API tokens.

For the corporate ones, the compliance nerds, or just curious nerds, based on your plan, you'd also see a ton of project-specific stats (like CDA hits, assets, IPs, etc.) and project audit logs here.

## Configurations

Configurations are a bit more aligned to content workflows. Aside from setting the language of the CMS and making your CMS prettier for you (THEMESSSS!) you can install and manage all the plugins you're working with.

Based on your users and roles, you can also modify permissions for each role here, as well as tweak your workflows. Draft to Published not good enough for you? Need to add in an `Approval Needed` stage? This is where those live.

[

(Image content)

Next episode

Intro to the Plugin Ecosystem

](https://www.datocms.com/user-guides/the-basics/intro-to-the-plugin-ecosystem.md)

---

# Intro to the Content Area

Source [user-guides]: https://www.datocms.com/user-guides/the-basics/intro-to-the-content-area.md

2m 35s

In this video we take our first look at the Content Area of DatoCMS - the main section where most of us would create, translate, modify, publish, and unpublish the content we're creating.

When starting a new project from scratch, this section doesn't exist, since we need a schema, or our content models, for us to fill up with content. So take a look at what it looks like in a relatively full-er project.

The sidebar is where you can navigate all the content types available to you based on your schema, such as blog posts, landing pages, authors, etc. These are completely customizable into folders as you need.

The content view or the content table itself is where you'd see all the records created within that model (example, all posts created for a blog).

You also get all the relevant information based on the fields of that model, such as the title, slug, description, content, etc., as well as some metadata, and can create custom filtered views for yourself (or your team) based on that information, for easier navigation within the CMS.

This is also where you can get a snapshot view of your content by their localizations, if you have localizations enabled in your project.

The Content Area is likely the section of the CMS you'd use the most, so we highly recommend playing around with all the options and getting familiar with it!

[

(Image content)

Next episode

Intro to the Schema Builder

](https://www.datocms.com/user-guides/the-basics/intro-to-the-schema-builder.md)

---

# Intro to this series

Source [user-guides]: https://www.datocms.com/user-guides/the-basics/intro-to-this-series.md

1m 32s

Welcome to the How to DatoCMS video series!

These are casual, non-technical walkthroughs of the CMS, intended as user guides, focused on the content creators, editors, and marketers.

We're going to be focusing mainly on all aspects of the UI, including the Schema Builder, Content Area, and Media Area. While we'll be very briefly brushing over some technical concepts like content modeling, image optimizations, and such in context, we'll keep the content purely non-technical to make it easy to follow along.

For the technical deets, our [Docs](https://datocms.com/docs) are always a great starting point.

So sit back as Ronak and Alessio take you through building a simple B2B SaaS website project from scratch, dropping easter eggs and covering topics like:

-   Building your schema
-   Creating Landing Pages
    
-   Creating Blog Posts
-   Linking Content Records
    
-   Managing Assets
-   Managing Localizations
    
-   Managing SEO
-   Managing Versions & Workflows
    
-   ... and a lot more.
    

They're going to be fumbling along as we all learn together, make mistakes, and come close to building a real-world project similar to the ones you'll be working on.

If there's anything you're missing on here, something that isn't too clear, or something that sounds like complete rubbish, share some feedback and we'll make improvements!

[

(Image content)

Next episode

Intro to the Content Area

](https://www.datocms.com/user-guides/the-basics/intro-to-the-content-area.md)

---

# Squishing big Plugin SDK bugs 🪲

Source [product-updates]: https://www.datocms.com/product-updates/squishing-big-plugin-sdk-bugs.md

[date: 2026-09-24T09:44:20.836+02:00]

We've gotten rid of a few pesky bugs that were getting in your way when building plugins:

**Dark mode inputs now match**. In dark mode, text boxes inside plugins were a different shade from the native DatoCMS text boxes, so plugins looked slightly off. That's no longer an issue.

**Plugin text size now matches the rest of the UI**. DatoCMS shrinks its base font size a tiiiiiny bit on smaller screens, but plugins always stayed at full size, so they looked wonky and oversized next to everything else. Plugins now follow the same sizing and adjust when the screen size changes.

**Typing lag in plugins is fixed**. This is (was?) the annoying one. The library that plugins use to talk to DatoCMS was doing a lot of unnecessary work on every message, which made typing feel sluggish when plugins were on the page. Processing time per message dropped from about 5–6ms to under 1ms, so typing is noticeably smoother.

**Plugins no longer reload when you resize the window**. Dragging the editor window across a certain width (1024px) made every plugin reload, and sidebar plugins could lose what was showing. They now stay put in place.

Special thanks to DatoCMS user [**convincible**](https://community.datocms.com/u/convincible/summary) for this, who reported and helped us troubleshoot all four issues!

---

# CDA jitters retries to prevent hitting rate limits

Source [product-updates]: https://www.datocms.com/product-updates/cda-jitters-retries-to-prevent-hitting-rate-limits.md

[date: 2026-09-18T15:00:53.664+02:00]

If you've ever triggered a big static build and watched it fall flat because of rate limits, this one's for you 🫶🏽

Earlier, with big builds, all those requests for pages/posts/etc., hit us at the same time, and some get rate-limited, so the client retried them. Plot twist, every retry *also* fired at the same instant. So we were constantly retrying in perfect lockstep.

To fix this, we've added jittering into the retries on the CDA client. Instead of all retries going out at once, we spread them across a small randomized window to stagger the requests.

If you run large static builds, this should slowly cut down your rate-limit errors.

You will need to `npm update @datocms/cda-client` for this to work on your project.

---

# Consent and permission updates to the DatoCMS MCP

Source [product-updates]: https://www.datocms.com/product-updates/clearer-consent-screen-and-permission-levels-for-mcp-clients.md

[date: 2026-09-14T10:53:47.245+02:00]

If you're connected to Claude, ChatGPT, Cursor or any other AI tool using the DatoCMS MCP, we've rolled out a few updates that should make things easier.

The first big change is how you authenticate your AI against our MCP server:

(Image content)

**The screen now tells you who is asking for permissions.** The name of the app is right in the title, together with the address the login will be sent back to. If the app you're installing isn't the one we've verified, the page says "Authorize an Unverified app" and highlights the destination in red, so you can stop and verify which app you're installing.

**You decide how much the app can do.** Besides picking which projects the app can access, you can now choose a level of access:

-   **Only read content**: read content and media, never write or edit anything.
-   **Read and edit content (recommended for editors)**: the app can also create, publish, and delete content and media. It can do nothing on schema, settings, users, or API tokens.
    
-   **Anything you can (recommended for devs)**: whatever your account can do in DatoCMS, the MCP can as well.
    

⚠️ Whichever option you pick is still restricted by your own user permissions in each project.

If you choose one access level, you can always revisit the config screen to edit this.

**If you're already using the MCP, you'll need to reconnect it once.** We've changed how the MCP server handles tokens: the token your AI tool receives now only works with the MCP server and is tied to the app that requested it, and the authorization code you get after signing in works one time. During your next session, Claude and ChatGPT will ask you to log in again. Other clients may need you to remove and re-add the DatoCMS connector.

With this new approach, the connection also remains logged in until you revoke it, unlike before, where you had to re-authorize the connection every 30 days.

---

# New Query Builder GUI For Audit Logs

Source [product-updates]: https://www.datocms.com/product-updates/new-query-builder-gui-for-audit-logs.md

[date: 2026-09-09T12:10:11.834+02:00]

Filtering audit logs in the past meant creating queries with PartiQL, which is... not fun. No autocomplete, no syntax hinting, no inline validation, nada. Minor mistakes made queries fail, and that's not something you want when filtering out critical logs. I mean if you're filtering audit logs you're probably not having the best day to begin with, so why make it worse 😅

To make it a little more "fun", we've added a new GUI for building a filtered query, which should look very familiar to you.

(Image content)

No more guessing whatever a `min_ulid()` is, whether to single or double-quote, or what the correct query path for the record model is... (hint: it was `request['payload']['data']['relationships']['item_type']['data']['id']`... 😬?)

Now, just pick a date range and click to choose your filters, and we take care of the rest.

ℹ️ Audit Logs are only available on our Enterprise plans. If you want to add it to your project, [get in touch](https://datocms.com/contact)!

---

# Improved plugin development & release workflows

Source [product-updates]: https://www.datocms.com/product-updates/improved-plugin-development-and-release-workflows.md

[date: 2026-09-10T10:22:25.482+02:00]

Currently, developing plugins is a liiiitle more cumbersome than just "install it and configure it": you fork one to build a new version, pin an exact release, check it locally, adopt a package... etc., etc.,.

We've replaced all of that with a proper set of workflows, grouped under a new **For developers** menu on every plugin.

(Image content)

  
Here's what the new workflows enable:

**Fork a plugin to develop against it safely.** "Duplicate for development" creates a private copy of any plugin, ready to point at your dev server. It offers to disable the original at the same time, so you don't end up with two near-identical entries wherever plugins show up while you're mid-development. The copy isn't assigned to any field, so if you need to test it where the original is in use, reassign that field to the copy by hand.

**Or skip duplicating altogether.** "Point to local server" allows you to point the existing plugin straight at your dev server without making a copy, and start making your changes on it. It detaches the plugin from npm (it becomes private, and stops receiving Marketplace updates) until you point it back with "Switch to Marketplace version".

**Bring a private plugin onto the Marketplace.** "Switch to Marketplace version" points a private plugin instance at an already-published package allowing you to publish the plugin to the marketplace. Its configured parameters and field assignments are left exactly as they are.

**"Switch to different version"** pins any Marketplace plugin version (pre-releases included). It's how you'd stage the rollout of a new release for a popular plugin: publish it to npm without tagging it `latest`, pin that exact version here to try it yourself on a real project first, and once you're happy with it, tag it `latest` on npm so it propagates to every other project tracking the package.

Aside from these, plugins **can now be disabled**. A disabled plugin behaves as if it were never installed: no code loads, fields fall back to their default editor, nothing renders. The configuration of the disabled plugin stays untouched, so you can isolate a slow or misbehaving plugin and switch it back on later.

---

# New text filter in Content View sidebar

Source [product-updates]: https://www.datocms.com/product-updates/new-text-filter-in-content-view-sidebar.md

[date: 2026-09-08T16:51:21.040+02:00]

It can get pretty tiring to scroll through all your record types when looking for something specific, so we've rolled out a new sidebar filter to make things easier.

(Image content)

Click on the search bar, start typing, and see a filtered list of models to create/edit content for.

---

# Announcing the new DatoCMS Remote MCP Server

Source [product-updates]: https://www.datocms.com/product-updates/announcing-the-datocms-remote-mcp-server.md

[date: 2026-05-26T12:15:58.083+02:00]

The DatoCMS MCP server is now hosted remotely! Log in once via OAuth and work across every project you have access to — in a single session, without restarting the server or swapping environment variables.

No `npm install`, no API token management! The server is always up to date, always available, and always secure.

(Video content)

This also means the MCP server is no longer a developer-only tool. With the local version, setting it up required terminal skills, npm, and manual token configuration — effectively limiting it to technical users. The remote server removes all of that: content editors, marketers, and anyone with a DatoCMS account can now use AI assistants to interact with their projects directly.

### Quick links

-   Get [used to working with the new MCP via the docs](https://www.datocms.com/docs/mcp-server.md)
-   If you're introducing your content team to working with the MCP in DatoCMS, [here's a complete user guide aimed at editors](https://www.datocms.com/user-guides/content-management/working-with-the-datocms-mcp.md)
    
-   [Check out the blog post for a few more details](https://www.datocms.com/blog/introducing-the-new-datocms-remote-mcp.md)
    

### What's new

-   **Scoped OAuth authentication:** No more explicit API tokens! During the authorization step, you can limit access to only the projects you choose. Every action is tied to your personal identity, giving teams clear visibility over who made which changes, when using an AI assistant.
-   **Multi-project support:** The agent can discover which projects you have access to, and switch between them within the same conversation. You can also paste a DatoCMS editor URL into a prompt and the server resolves the right project automatically.
    
-   **Isolated script execution:** Scripts execute in an isolated remote runner (not on your machine), plus your DatoCMS credentials are kept safe and cannot be read by the agent.
-   **Separate tools for safe/unsafe actions:** You can configure your agent to ie. always execute safe (read-only) actions, and manually confirm writes/deletions.
    

### Migration

Setup is simpler than ever! Follow the [installation guide](https://www.datocms.com/docs/mcp-server.md#installation) for your specific AI client — most of the times, this is the snippet that works:

```json
{
  "mcpServers": {
    "datocms": {
      "type": "http",
      "url": "https://mcp.datocms.com"
    }
  }
}
```

### Breaking changes

The old local MCP server **has been deprecated** in favor of the new, improved remote MCP server. The [Github repository](https://github.com/datocms/mcp) is archived and no longer maintained.

---

# Custom colour palettes are being deprecated

Source [product-updates]: https://www.datocms.com/product-updates/custom-colour-palettes-are-being-deprecated.md

[date: 2026-08-24T18:32:50.665+02:00]

### **What's changing**

Starting from the 25 Aug, 2026, the custom palette under Configuration \> Appearance won't be available as an option for new projects — it will only be possible to use monochromatic palettes. Pick a hue, and the rest of the palette (backgrounds, text, accents) is generated for you automatically, tuned for readability.

If your project was created before that date, nothing changes for you: custom palettes remain available, whether you're already using one or not.

### **Why we're making this change**

Since early on, DatoCMS wanted to offer a way to customise the theme of a project's admin area, to give you as much creative freedom as possible over how your admin area looked. Over time, though, we heard from editors — the people actually spending their day inside these interfaces — that some custom palettes were tiring to read comfortably. After some research, we found that colour is harder to get right than it looks: contrast ratios, how a background reads against small text, how a palette holds up across every screen in the product.

That's the main reason we introduced the [monochromatic system in 2024](https://www.datocms.com/product-updates/introducing-monochromatic-palette-for-projects.md): it solves that problem by design. Every colour in the palette — text, backgrounds, accents — is engineered to meet WCAG AA contrast standards, so whatever hue you pick, the result stays comfortable to read. We've now decided to make it the standard for every project, keeping that quality bar guaranteed while still leaving room to personalize it.

---

# Easier recognition for official plugins

Source [product-updates]: https://www.datocms.com/product-updates/easlier-recognition-for-official-plugins.md

[date: 2026-08-11T14:10:16.832+02:00]

The [DatoCMS Marketplace](https://www.datocms.com/marketplace/plugins.md) now shows you which plugins are "officially" developed by us with a little icon next to each one.

While this definitely makes it easier to find, official plugins are also our responsibility to maintain and keep up to date: so if something breaks or works unexpectedly, just give us a ping.

They're all categorized as a section within the [Marketplace](https://www.datocms.com/marketplace/plugins.md) on the website and in the CMS.

(Image content)

And you can easily tell it is an official plugin with the little icon.

(Image content)

---

# Quick search by Record ID in the CMS

Source [product-updates]: https://www.datocms.com/product-updates/introducing-quick-search-by-record-id.md

[date: 2026-08-07T11:08:14.729+02:00]

Sometimes all you get from a terminal output is a Record ID and nothing else. Made it kinda hard to find that record in the UI if you wanted to check for something. The filter by ID wasn't the best way, especially if you didn't know the item type.

So we've rolled out the ability to search by ID ✨

(Image content)

Open the UI, click the 🔍, enter the Record ID, and it'll surface right in the UI.

---

# Filter by creators in the CMS

Source [product-updates]: https://www.datocms.com/product-updates/filter-by-creators-in-the-cms.md

[date: 2026-08-17T10:03:51.504+02:00]

We've added a small but useful enhancement into the CMS, allowing you to filter records by creator.

(Image content)

Simply add a new filter, and as with all others, find `Creator` available as a new option.

---

# Visual editing now works in sidebar previews too

Source [product-updates]: https://www.datocms.com/product-updates/visual-editing-now-works-in-sidebar-previews-too.md

[date: 2026-08-10T09:29:40.366+02:00]

Until now, using [Visual Editing](https://www.datocms.com/features/visual-editing.md) meant clicking something in your preview and jumping straight to the record that renders it. It only lived in the iframe inside the plugin's **Visual** tab.

Now you can turn it on in the sidebar preview in the **Content** tab as well 👀

(Video content)

Click an element in the preview and you land right on `/editor/item_types/<id>/items/<id>` within the exact field behind it without hunting through the tree.

A few things worth knowing:

-   It's a **per-user toggle, off by default:** each editor flips it on for themselves, so it won't change anyone else's setup.
-   When the toggle is on, the sidebar loads the draft-mode preview URL (that's what visual editing needs to hook into).
    
-   If the selected preview link's frontend doesn't support visual editing, the toggle stays visible but disabled, with a tooltip explaining why.
    

Under the hood it's the same Content Link machinery that already powers the Visual tab, just wired into the sidebar frame, so [refer to the docs to set up Visual Editing](https://www.datocms.com/docs/visual-editing.md).

---

# Better discoverability in content record sidebars

Source [product-updates]: https://www.datocms.com/product-updates/better-discoverability-in-content-record-sidebars.md

[date: 2026-08-17T09:41:32.564+02:00]

It was a bit difficult to find where your sidebar plugins ended up in the content record sidebars.

Earlier, you had to open the sidebar, find the little downward pointing arrow and then switch to the view that plugin offered.

(Image content)

It caused a lot of issues for many of you, and wasn't really the best UX, so we've improved it.

Now, simply open the sidebar if its collapsed, and you'll see a tab for each of your sidebar plugins neatly next to one another.

(Image content)

---

# Real-time API is now more real(er?)-time 🏎️

Source [product-updates]: https://www.datocms.com/product-updates/real-time-api-is-now-more-realer-time.md

[date: 2026-08-05T16:51:55.482+02:00]

If you've been building on our [Real-time API](https://www.datocms.com/features/real-time-api.md), you know the drill: you fire a mutation, then wait ~1.5–2.5s before the change lands in the browser.

That wasn't very "real time", so we dug into speeding things up, and that wait is gone.

Updates now show up in roughly ⚡️ **100–500ms ⚡️**

(Video content)

Get into the [docs to implement the Real-time API](https://www.datocms.com/docs/real-time-updates-api.md).

---

# Turn off Content Link encoding for specific fields

Source [product-updates]: https://www.datocms.com/product-updates/turn-off-content-link-encoding-for-the-fields-that-cant-afford-it.md

[date: 2026-07-22T14:44:12.301+02:00]

You can now disable Content Link (visual editing) encoding on a per-field basis, right from the field settings. Fields whose exact value matters — slugs, external IDs, keys, anything compared verbatim — can opt out, so the Content Delivery API never wraps their value in invisible visual-editing metadata.

### A quick refresher on how Content Link works

[Visual editing](https://www.datocms.com/docs/visual-editing.md) lets editors click directly on any element of your live site and jump straight to the field that controls it. It works through **steganography**: when you request draft content with Content Link enabled, the Content Delivery API embeds invisible Unicode characters into your text fields, encoding which record and field produced each string. Your `<ContentLink />` component reads that metadata and paints the clickable overlays. Visually, nothing changes, but that invisible metadata is exactly what you *don't* want on certain fields.

### The problem

Because the encoded value carries extra (invisible) characters, it's no longer byte-for-byte identical to what you typed. That's harmless for prose, but it breaks anything that treats a field as a literal:

-   A `slug` field with the value `about` no longer `===` `"about"`, so routing and equality checks fail.
-   IDs, keys, and tokens used in comparisons, `switch` statements, CSS selectors, or `data-` attributes silently misbehave.
    

Until now, the fix meant calling `stripStega()` on the values that needed it. That still works, but it's easy to forget and easy to miss one.

### What's new

Open any **string**, **multiple-paragraph text**, or **structured text** field's settings, and you'll find a new toggle:

(Image content)

Leave it on for the fields your editors want to click into. Turn it off for the fields that need to stay verbatim, and the CDA will exclude that field from Content Link encoding entirely and the value ships clean, no `stripStega()` required.

The toggle is enabled by default, so nothing changes for your existing projects unless you opt a field out.

### Under the hood

-   A new `content_link_enabled` attribute is exposed on the field, defaulting to `true`.
-   The opt-out is available on the field types that participate in Content Link encoding today: `string`, `text`, and `structured_text`.
    
-   When disabled, the exclusion happens server-side in the Content Delivery API, so it applies no matter which client or framework SDK you use.

---

# A new layer of security: email verification

Source [product-updates]: https://www.datocms.com/product-updates/a-new-layer-of-security-email-verification.md

[date: 2026-06-25T11:51:03.280+02:00]

We've added a small but meaningful safeguard to DatoCMS accounts: email verification. Before a handful of sensitive actions, we now make sure the email address on your account really belongs to you. It runs quietly in the background, and most of the time you won't even notice it.

#### When it kicks in

We only ask for a verified email before actions that affect other people or move projects between accounts:

-   Inviting someone to your organization
-   Transferring a project to another account or organization
    
-   Accepting a project that's being transferred to you
-   Joining a project or organization you've been invited to
    

#### How it works

When you sign up, we send a confirmation email. Click the link inside and you're verified for good.

If you reach one of these actions before you've verified, we'll pause and send that email for you automatically (or resend it, if it's been a while). Click the link, and we'll pick up right where you left off, with nothing to redo and nothing to start over. You'll only go through this once.

---

# Dark mode for your dashboard 🌚

Source [product-updates]: https://www.datocms.com/product-updates/dashboard-dark-mode.md

[date: 2026-06-25T11:35:47.556+02:00]

Dark mode is now available in your DatoCMS dashboard, too. Manage your account, projects, and billing in a theme that's easier on the eyes during long sessions.

Choose **System**, **Light**, or **Dark** from the menu under your avatar. Your preference is saved to your account, and the dashboard follows your OS setting until you pick a side.

---

# See exactly when each version of a record was live

Source [product-updates]: https://www.datocms.com/product-updates/see-exactly-when-each-version-of-a-record-was-live.md

[date: 2026-06-17T19:25:47.199+02:00]

The version history of a record now doubles as a publication timeline. Every version tells you not just when it was created, but the exact window it was actually published (and a green dot shows you which version is currently published).

Before this, the version history panel only showed when a version was saved and by whom. This change makes it simpler to understand when they were published.

(Image content)

Asset history

### What's new

Open any record's history, and each version now carries its publication info under the timestamp:

-   *Live since X* — this version is published and currently live, with no end in sight.
-   *Live from X → Y* — this version was public for a stretch, then superseded.
    
-   *Live on X, HH:MM → HH:MM* — published and replaced the same day, down to the minute.
    

A redesigned visual indication ties it all together:

-   A green "live" dot marks the version that's currently published.
-   A muted dot marks versions that were live at some point in the past.
    
-   Each entry now credits the editor who made the change with a "*Edited by …"* so the timeline reads as a full audit trail at a glance.
    

### Under the hood

The publication ranges are backed by new metadata on item versions — `published_from` and `published_until` — exposed through the Content Management API. The dashboard reads them via `@datocms/cma-client` 5.5.1 or higher.

This is a forward-looking history: we don't retroactively reconstruct intervals for versions that were already live when the feature shipped.

If you publish a version, unpublish, then re-publish that *same* version with no edit in between, the new `published_from` overwrites the previous one and `published_until` is cleared. The version reflects its latest interval rather than a full multi-interval log. In practice most workflows save an intermediate version before re-publishing, which produces a fresh row and preserves the earlier one.

---

# Responsive images that size themselves, across every SDK

Source [product-updates]: https://www.datocms.com/product-updates/responsive-images-automatic-sizes.md

[date: 2026-06-12T12:29:38.214+02:00]

Getting correctly-sized responsive images used to mean adding a `sizes` prop that mirrored your CSS layout, and keeping it in sync as the design changed. No more. Update any DatoCMS framework SDK and you can stop dealing with that.

### Responsive images were a burden

The whole point of a responsive `srcset` is to let the browser pick the smallest image that still looks sharp. But to choose, it has to know how wide the image will actually render, and it needs that number up front, while parsing the HTML, before any CSS or layout exists.

It can't measure the element itself, so the job fell on you: hand-write a `sizes` value mirroring your CSS layout, and keep it in sync every time the design changed.

### Hello, `sizes=auto`!

Browsers now have a proper fix: `sizes="auto"`. On a lazily-loaded image, it tells the browser to use the element's real, laid-out width when choosing a `srcset` candidate. Because a lazy image is fetched *after* layout, the box is already measured by the time the request goes out, so the choice is exact, not estimated.

When you don't pass an explicit `sizes` prop, all four framework SDKs: `react-datocms`, `vue-datocms`, `@datocms/svelte`, and `@datocms/astro`, now emit `sizes="auto"` (with `100vw` kept as a fallback) together with `loading="lazy"`:

```html
<!-- before -->
<img srcset="… 200w, 400w, 800w, 1600w" sizes="100vw" loading="lazy" />

<!-- now -->
<img srcset="… 200w, 400w, 800w, 1600w" sizes="auto, 100vw" loading="lazy" />
```

Upgrade the package and your existing `<Image>` components start requesting right-sized files on their own.

### Under the hood

-   If you pass an explicit `sizes` prop, or your `responsiveImage` GraphQL query already returns a `sizes` value, we never override it.
-   Images marked with the `priority` prop load eagerly, and `sizes="auto"` requires `loading="lazy"`, so they keep their current behavior.
    
-   Every SDK component already sets `aspect-ratio` + `width: 100%` + a `max-width`, which is exactly what `auto` needs to resolve to the correct box, so the default styling already does the right thing.
    

### Browser support and graceful fallback

`sizes="auto"` is supported in Chrome and Edge 126+, Opera, Samsung Internet, and Firefox 150+. Safari doesn't support it yet.

That's why we emit `sizes="auto, 100vw"` rather than a bare `auto`: browsers that don't understand `auto` skip it and fall back to `100vw` — the safe default that's been the de-facto, so there's no regression anywhere.

Upgrade the SDKs to pick it up:

Terminal window

```bash
npm i react-datocms@latest      # React
npm i vue-datocms@latest        # Vue
npm i @datocms/svelte@latest    # Svelte
npm i @datocms/astro@latest     # Astro
```

### Further context

-   [MDN — the `sizes` attribute and the `auto` keyword](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#sizes)
-   [Can I Use — `sizes="auto"` browser support](https://caniuse.com/wf-sizes-auto)
    
-   [Chrome Platform Status — Auto Sizes for Lazy Loaded Images with Srcset](https://chromestatus.com/feature/5191555708616704)
-   [Cloudfour — The catch with `sizes="auto"`](https://cloudfour.com/thinks/ending-responsive-images/)

---

# Pick the exact frame that represents your video

Source [product-updates]: https://www.datocms.com/product-updates/pick-the-exact-frame-that-represents-your-video.md

[date: 2026-06-17T15:05:16.967+02:00]

**Heavy on video usage and want to set a specific frame as the thumbnail from within the CMS itself? Now you can.**

TL;DR: You can now choose the poster frame for any video right from the asset editor. Choose the perfect moment, pin it, and that frame becomes the video's thumbnail everywhere. No re-encoding, no external tools, no guessing which frame the player will grab.

### How it works

(Image content)

Open a video asset and you'll find a poster picker built right into the player:

Head to the frame you want**.** A pin on the seek bar marks the current poster frame, aligned precisely to the player's timeline so what you see is what you get.

Hover the pin and a large, high-resolution thumbnail shows exactly how that frame will look as the poster, sized to fit whether you're in the media area, a modal, or the sidebar.

Select **Use current frame as poster** and your choice saves instantly.

### Under the hood

The chosen frame is stored as `poster_time` (in seconds) inside the upload's `default_field_metadata`, the direct video analogue of `focal_point`. Like the focal point, it's a non-localized, per-asset default, so it travels with the asset and is available through the Content Management API for your front end to consume when rendering posters.

Thumbnail selection is precise to the hundredth of a second**.** The time readout shows `m:ss.cs`, so you can land on an exact frame rather than a rough area — `1:04.27`, not "somewhere around 1:04".

---

# A tidier CMA format for focal points

Source [product-updates]: https://www.datocms.com/product-updates/non-localized-focal-points.md

[date: 2026-06-10T16:08:20.794+02:00]

Focal points are now non-localized: every asset has a single focal point, shared across all locales. That change is already live in every project and asks nothing of you. (If you missed it, [here's the full story](https://www.datocms.com/product-updates/one-focal-point-per-asset.md).)

This post is about the one part that *is* opt-in: the shape of `default_field_metadata` in the Content Management API.

By default, nothing changes for your integrations. The CMA keeps returning and accepting the existing locale-keyed shape, so your current code keeps working untouched. The only difference is that the focal point is now the same value in every locale entry: the one real value, replicated for backward compatibility.

When you want an API that reflects the new reality, opt in. With the flag on, `default_field_metadata` switches to a field-keyed shape: `alt`, `title,` and `custom_data` remain locale-keyed, but focal\_point appears once at the top level. The legacy locale-keyed shape is then no longer accepted on write, so update any code that writes `default_field_metadata` to the new shape before you activate it.

#### **Before opt-in migration: Current locale-keyed shape**

```json
{
  "data": {
    "id": "12345678",
    "type": "upload",
    "attributes": {
      "default_field_metadata": { // Locales first, then fields
        "en": { // Primary locale
          "alt": "English alt text",
          "title": "English title",
          "custom_data": {
            "english_custom_data": "hi"
          },
          "focal_point": {
            "x": 0.12,
            "y": 0.34
          }
        },
        "it": {
          "alt": "Italian alt text",
          "title": "Italian title",
          "custom_data": {
            "italian_custom_data": "ciao"
          },
          "focal_point": { // This is silently ignored now; overridden by primary
            "x": 0.56,
            "y": 0.78
          }
        }
      }
    }
  }
}
```

#### After opt-in migration: New field-keyed shape

```json
{
  "data": {
    "id": "12345678",
    "type": "upload",
    "attributes": {
      "default_field_metadata": { // Fields first, then locales
        "alt": {
          "en": "English alt text",
          "it": "Italian alt text"
        },
        "title": {
          "en": "English title",
          "it": "Italian title"
        },
        "custom_data": {
          "en": {
            "english_custom_data": "hi"
          },
          "it": {
            "italian_custom_data": "ciao"
          }
        },
        "focal_point": { // No longer locale-specific
          "x": 0.12,
          "y": 0.34
        }
      }
    }
  }
}
```

### Activating the opt-in

#### Existing Projects (created before 2026-06-11)

In an environment's **Configuration** screen (not Project settings), under **Available updates**, you should see a new opt-in update:

(Image content)

It's set per environment, and it's a one-way switch: once `non_localized_focal_points` is on, you can't turn it back off. If you have integrations that write `default_field_metadata` through the CMA, we strongly recommend enabling it first in a separate Sandbox environment, updating and verifying your integrations there, and only then activating it on your primary environment.

If you don't write this data programmatically, there's nothing to do here. The default shape continues to work, and the focal point is already consolidated across all assets.

#### New projects (created after 2026-06-11)

The opt-in is automatically enabled for new projects and will use the new field-keyed format.

*JS clients' support for the new shape is available from* [*version 5.5.0*](https://github.com/datocms/js-rest-api-clients/releases/tag/v5.5.0)*.*

---

# One focal point per asset

Source [product-updates]: https://www.datocms.com/product-updates/one-focal-point-per-asset.md

[date: 2026-06-11T15:34:58.818+02:00]

The focal point of an image marks where its subject is: the face, the product, the thing that should stay in frame. It matters when you ask GraphQL for a cropped version. Pass `imgixParams` like `w: 200, h: 200, fit: crop`, and if a focal point is set, DatoCMS automatically adds `fp-x` and `fp-y` so the crop is built around the subject rather than the dead center of the image. You set it by clicking on the image in the upload's preview.

When we first shipped this, we let you set a different focal point for each locale, behind an interface that did almost everything in its power to hide that fact. The control sat on the left, over the image. Which locale you were actually editing was decided somewhere else entirely: a language selector tucked into the *Default metadata* panel on the right, with nothing visibly connecting the two. The only hint was a small *Focal point (English)* label that changed quietly when you switched languages on the far side of the screen.

We were wrong, about both the idea and the interface. A focal point describes where the subject physically sits in the pixels, and a face doesn't move when you translate the page into German. And because the localizability was effectively invisible, almost nobody used it. Across more than 20 million multi-locale uploads, 92% had a focal point set in the primary locale only. That was less a deliberate choice than the natural result of an interface that never let on there was anything else to set. Of the few who did discover the per-locale behaviour, 95% simply repeated the same coordinates across all locales. Just 0.019% ever set genuinely different focal points across locales, and even then, the differences were almost always too small to see.

Worse, making it localizable didn't only add friction. It quietly broke cropping. Because nearly everyone set the point in a single language, focal-point cropping worked in that language and silently fell back to a plain centre crop everywhere else. The subject you carefully framed in English could end up off-centre, or cropped out entirely, in Italian, with nothing in the editor to warn you.

So we've fixed it.

(Image content)

### What changes for everyone today

Every asset now has a single focal point: one value, shared across all locales. This isn't an opt-in. It applies to every project right away.

Cropping is now consistent across languages. Set the focal point once, and it applies in every locale. If you were only setting it in your primary locale (as most people were), your crops everywhere else just improved on their own, with nothing for you to do.

The editor is simpler, too. The focal point is no longer tied to the language selector or to the *Default metadata* form. It's now an always-visible control on the upload's preview. Click anywhere on the image to aim it, and the new position saves instantly in the background: no language to pick, no Save button to hunt for. (That also retires the old gotcha where the Save button hid inside a collapsible panel and was easy to lose after you'd moved the point.)

On the CDA, the response shape doesn't change. `focalPoint` is still a single value per query. It just now resolves to that one shared value in every locale, instead of vanishing in the locales where no one had set it.

In the rare case where you'd deliberately set different focal points per locale, the value from your primary locale is the one we keep. If you need the old per-locale values restored, contact Support.

*If you read or write focal points through the Content Management API, there's a short follow-up for you: an optional, tidier CMA format.* [*Read it here.*](https://www.datocms.com/product-updates/non-localized-focal-points.md)

---

# Hello Dark Mode 🌚

Source [product-updates]: https://www.datocms.com/product-updates/hello-dark-mode.md

[date: 2026-06-05T10:11:01.465+02:00]

You and your editors can now use DatoCMS in dark mode. It follows your OS preference automatically: switch your system to dark and DatoCMS switches with you, no configuration needed.

Not a fan of that? Go manual. Each team member sets their own preference independently, synced to their account across all devices, with the option to override their system setting anytime.

(Video content)

Dark mode covers every single view: Schema Builder, Record Editor, Media Library, Settings pages, all of it. Contrast ratios were checked for WCAG compliance so it's built for long editing sessions, not just a cosmetic fresh coat of paint.

### Upgrading Plugins

If you maintain a plugin, this part's for you — otherwise you can skip it.

In dark mode, plugins built on datocms-plugin-sdk 2.1.5 or earlier render inside a white frame. They keep working as normal; the frame is just a safe fallback so nothing breaks until you update.

To make a plugin fully dark-mode compatible, upgrade to the latest SDK version and follow [this upgrade guide](https://www.datocms.com/docs/plugin-sdk/upgrading-plugins-for-dark-mode.md). The migration is mechanical, so the guide includes a **ready-made prompt you can hand to an AI agent** to do the whole thing for you.

---

# CLI: \`npx datocms\` now Just Works

Source [product-updates]: https://www.datocms.com/product-updates/cli-npx-datocms-now-just-works.md

[date: 2026-04-29T16:42:07.831+02:00]

The DatoCMS CLI is now published on npm as `datocms` — an unscoped package with the same name as its binary. The scoped `@datocms/cli` package is still around as a thin alias, so existing setups keep working unchanged.

**The main win:** `npx datocms` now "Just Works" in every context — whether the package is installed locally, globally, or not installed at all.

### Why this matters

Until now, running `npx datocms projects:list` inside a project that had `@datocms/cli` installed could result in a confusing error:

Terminal window

```bash
$ npx datocms projects:list
npm error Missing script: "datocms"
```

This is an npm/npx quirk: when the package name and binary name differ, it can't always find the CLI and bails out. `pnpm`, `yarn`, and `bun` all handled this case correctly — it's only npm users who got bitten. Now that the package and binary share the same name, the most natural command just works. As a bonus, we now own the unscoped `datocms` name on npm, so it can't get squatted.

### What changes for you

-   **New users:** run `npx datocms ...` or `npm i -g datocms`. That's it.
-   **Existing users on** **`@datocms/cli`****:** nothing to do. Your setup keeps working, and you'll transparently pick up the new `datocms` package as a dependency. Migrating is a one-line change in `package.json` whenever you feel like it.
    
-   **Existing migration files** importing from `@datocms/cli/lib/cma-client-node`: still work, no changes needed. New migration files use `datocms/lib/cma-client-node`.
    

Head over to the [docs on configuring the CLI](https://www.datocms.com/docs/cli.md) for installation details.

---

# Updated JS Client for end-to-end type safety

Source [product-updates]: https://www.datocms.com/product-updates/the-js-client-is-now-completely-rewritten-in-typescript.md

[date: 2025-10-10T11:46:07.309+02:00]

The DatoCMS JavaScript client is now fully type-safe. Records (the missing piece that wasn't) are also generated directly from your project’s schema.

(Video content)

This gives you real autocomplete and compile-time safety across your project.

(Video content)

No docs were opened in the making of this demo.

The CLI bridges your schema and repo perfectly. Run a single command to generate types, and if your schema changes, rerun it to keep your code perfectly in sync.

Terminal window

```bash
$ npx datocms schema:generate schema.ts
```

The client also ships with new utilities to simplify record management, like `duplicateBlockRecord()`, `inspectItem()`, and `mapBlocksInNonLocalizedFieldValue()` to handle nested blocks, relations, and localized fields automatically.

**Quick Links:**

-   [CMA Client Package](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client)
-   [Configuring the CLI Docs](https://www.datocms.com/docs/cli.md)
    
-   [Using the JS Client Docs](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients.md)
-   [Type-safe Development with TypeScript Docs](https://www.datocms.com/docs/content-management-api/resources/item.md#type-safe-development-with-typescript)
    
-   [Announcement post on the blog](https://www.datocms.com/blog/records-finally-typed.md)

---

# New CLI command to call any DatoCMS API method directly

Source [product-updates]: https://www.datocms.com/product-updates/new-cli-command-to-call-any-datocms-api-method-directly.md

[date: 2025-12-04T13:15:51.585+01:00]

You can now run any DatoCMS API method straight from [our CLI](https://www.datocms.com/docs/cli.md) with the new `cma:call` command. No need to write custom scripts anymore, just call the method you need directly from your terminal.

The command dynamically discovers all available resources and methods from `@datocms/cma-client`, so you're always working with the latest API surface.

(Video content)

**Key features:**

-   **Dynamic discovery:** All resources and methods are automatically available
-   **Smart validation:** Validates request body requirements and prompts for `--data` when needed
    
-   **Flexible parameters:** Supports URL placeholders via positional arguments and query parameters via `--params`
-   **Helpful errors:** Clear error messages with suggestions when something goes wrong
    

Perfect for quick API calls, testing, automation scripts, or when you just need to run a one-off command without spinning up a full script!

---

# CLI: Easier (and safer) project linking with OAuth

Source [product-updates]: https://www.datocms.com/product-updates/cli-easier-and-safer-project-linking-with-oauth.md

[date: 2026-04-14T11:48:08.092+02:00]

Setting up the DatoCMS CLI used to involve a clunky ritual of creating an API token in project settings, copying it, pasting it into an environment variable, and hoping you got it right. Repeat for every project. And since teams shared the same token, there was no way to tell *who* actually performed a given action in audit logs.

**OAuth login is now the recommended way to authenticate.** The new setup is fully guided: `datocms login` opens your browser, `datocms link` lets you search and select a project interactively with no tokens to copy, and no environment variables to configure. Every API call is tied to your personal identity, giving teams clear visibility over who made which changes.

During the authorization step, you can choose to grant the CLI access to all your projects or limit it to only the ones you select, so you stay in control of exactly which projects are exposed.

(Video content)

### New commands

-   `datocms login` authenticates your DatoCMS account via OAuth. It opens your browser for a secure login flow. If the browser can't be opened, it falls back to a manual URL flow.
-   `datocms link` connects the current directory to a specific DatoCMS project. The interactive flow walks you through choosing a workspace, searching for a project, and configuring migration settings. Once linked, every CLI command in that directory automatically resolves an API token using your OAuth credentials. No environment variables needed.
    
-   `datocms logout` to remove credentials.
-   `datocms whoami` to check which account you're logged in as.
    
-   `datocms unlink` to disconnect a directory from a project.
    

### What changes for existing users

Nothing breaks. The old `profile:set` and `profile:remove` commands still work, they just redirect to `link` and `unlink` under the hood. Existing scripts and CI/CD pipelines using `DATOCMS_API_TOKEN` or the `--api-token` flag are completely unaffected.

When a command needs an API token, the CLI now resolves it in this order: `--api-token` flag first, then environment variable, then linked project via OAuth. So your current setup always takes precedence.

### Getting started

You can upgrade the CLI to the latest version with:

Terminal window

```bash
npm install -g @datocms/cli@latest
```

For teams that want user-level audit trails, the migration is straightforward: each team member runs `datocms login` once, then `datocms link` in each project directory. From that point on, all commands use personal credentials automatically.

You can still use `DATOCMS_API_TOKEN` in CI/CD or anywhere OAuth login isn't practical. If you prefer a custom environment variable name, you can configure it during `datocms link`.

OAuth authorizations can be reviewed and revoked at any time from your [account settings](https://dashboard.datocms.com/personal-account/account), under "Authorized applications":

(Image content)

[Explore the docs](https://www.datocms.com/docs/cli.md) to get up to speed on Configuring the CLI.

---

# Introducing DatoCMS Agent Skills

Source [product-updates]: https://www.datocms.com/product-updates/introducing-datocms-agent-skills.md

[date: 2026-05-26T12:15:03.392+02:00]

DatoCMS Agent Skills are a set of markdown-based playbooks for AI coding agents. They give your agent the context it needs help you develop your projects correctly with DatoCMS: the right patterns, the right conventions, loaded on demand based on what you're working on.

**Quick Links:**

-   [Check out the docs and installation guide](https://www-draft.datocms.com/docs/agent-skills) to get started with your tooling preference.
-   The whole thing is open source at [github.com/datocms/agent-skills](https://github.com/datocms/agent-skills).
    
-   All [skills are also listed on the skills.sh directory](https://skills.sh/?q=datocms).
-   [Check out the blog post](https://www.datocms.com/blog/the-new-datocms-agent-skills.md) for a few further details.
    

Its' helpful knowing what these skills are capable of, so here's a TLDR of what they can currently do:

-   **Content modeling** — schema-design decisions: model vs block, references vs embedded blocks, taxonomies, field shapes, validators, editor appearances.
-   **Reading content** — GraphQL queries against the Content Delivery API: filters, pagination, localization, modular content, Structured Text, responsive images, SEO metadata, typed queries with gql.tada or codegen.
    
-   **Writing content & automation** — programmatic CMA scripts: record CRUD, bulk imports/exports, asset uploads, environment forks and promotions, webhooks, roles and tokens, scheduled publishing, audit logs.
-   **Workflows best practices** — migrations, schema-type generation, typed CMA scripts, environment operations, CI/CD pipelines.
    
-   **Frontend integrations** — draft mode, Web Previews, Visual Editing, real-time preview subscriptions, cache-tag invalidation, SEO/sitemap wiring across Next.js, Nuxt, SvelteKit, Astro.
-   **Plugin** **development** — create a brand-new plugin from scratch with the Vite/React structure, picking the initial surfaces (field extensions, config screens, sidebars, pages, asset sources).
    

**Installation**

For Claude Code and Codex:

Terminal window

```bash
/plugin marketplace add datocms/agent-skills
/plugin install datocms@datocms-skills
```

For Cursor, Windsurf, Copilot, and others:

Terminal window

```bash
npx skills add datocms/agent-skills
```

Skills are open source at [github.com/datocms/agent-skills](https://github.com/datocms/agent-skills) and listed on the [skills.sh directory](https://skills.sh/?q=datocms).

[Read the Agent Skills docs](https://www.datocms.com/docs/agent-skills.md) for full installation options and usage.

---

# CLI and CMA Improvements: cma:script, Stricter Types, and Dastdown

Source [product-updates]: https://www.datocms.com/product-updates/cli-and-cma-improvements-cmascript-stricter-types-and-dastdown.md

[date: 2026-05-18T15:41:44.751+02:00]

The last couple of weeks brought a set of converging improvements across the CLI, the JS CMA client, and the structured-text packages.

#### `datocms schema:generate` now exposes runtime ID and REF constants

The schema types generator now emits `Schema.Article.ID` and `Schema.Article.REF` alongside the existing TypeScript types. No more hardcoded item-type ID strings drifting out of sync across environments!

```typescript
// type position: the model's TS shape, as before
const article = await client.items.find<Schema.Article>(id);

// value position: the model's id and ref, generated from your project
await client.items.create({
  item_type: Schema.Article.REF,
  // …
});

if (item.relationships.item_type.data.id === Schema.Article.ID) {
  // …
}
```

#### New `FieldValue<T, K>` helpers

Three new helpers let you derive a field's type directly from an existing record or block for the full read/write cycle:

-   `FieldValue<T, K>` — standard response shape
-   `FieldValueInNestedResponse<T, K>` — what you get when reading with `nested: true`
    
-   `FieldValueInRequest<T, K>` — what `client.items.create` / `update` expects
    

`T` accepts anything item-shaped like a fetched record, a nested block, or an `ItemTypeDefinition` directly. The typical flow this unlocks: read a record with `nested: true`, iterate and mutate its nested blocks, and write the result back. Typing the accumulator takes one line:

```typescript
const page = await client.items.find<LandingPage>(id, { nested: true });
const sections: FieldValueInRequest<typeof page, 'sections'> = [];
```

#### New `isBlockWithItemOfType` predicate

`isBlockWithItemOfType` (and its `isInlineBlockWithItemOfType` counterpart) from `datocms-structured-text-utils` narrows a block node to a specific model shape using its item type ID. Works in `findFirstNode`, `mapNodes`, or standard conditionals, giving you typed access to `.item.attributes` directly without extra casting.

```typescript
// inline guard
if (isBlockWithItemOfType(Schema.CtaBlock.ID, node)) {
  // node.item.attributes is now typed as CtaBlock
}

// curried predicate
const firstCta = findFirstNode(content, isBlockWithItemOfType(Schema.CtaBlock.ID));
```

#### Edit Structured Text as plain text with `datocms-structured-text-dastdown`

The new `datocms-structured-text-dastdown` package gives you a lossless, markdown-flavored serialization for DatoCMS Structured Text documents. Serialize to plain text, edit it however you like (string manipulation, regex, LLM rewrite), and parse back instead of walking the AST.

Best fit: text-heavy content like articles, docs, and chapters where edits are textual and may cross node boundaries. Not for landing pages made of opaque blocks — referenced blocks stay opaque in the serialized form, so you can move or remove them but not edit their internals at this layer.

A typical round-trip would look like:

```typescript
import { parse, serialize } from 'datocms-structured-text-dastdown';

const cur = await client.items.find<Schema.Article>('article-id', { nested: true });

const text = serialize(cur.body);
const edited = text.replace(/Acme Corp/g, '**Acme Inc.**');
const body = parse(edited, cur.body);

await client.items.update<Schema.Article>('article-id', { body });
```

#### `mapNodes` now supports full tree rewrites

`mapNodes` and `mapNodesAsync` from `datocms-structured-text-utils` now support structural transforms, not just 1:1 node mapping. The return value controls what happens:

-   Return a single node: direct replacement
-   Return an array: splat into the parent's children
    
-   Return `null` or `undefined`: remove the node entirely
    

#### New agent-targeted commands

We've also shipped `datocms cma:script`, `datocms schema:inspect`, and enhanced `datocms cma:docs` - commands designed to be used by your agents directly. These are documented in the CLI repo and will evolve as agent tooling matures.

---

To update: `npm i -g datocms` for CLI changes, `npm i @datocms/cma-client-node@latest` for client changes, `npm i datocms-structured-text-dastdown` for the new package.

---

# CMA limit raised for Developer Plan

Source [product-updates]: https://www.datocms.com/product-updates/cma-limit-raised-for-developer-plan.md

[date: 2026-04-20T11:08:34.171+02:00]

We have raised the CMA limit of the developer plan from 10K to 25K monthly API calls, to make it easier to get started with projects on the free plan.

---

# Starter kits now ship with a plugin scaffold

Source [product-updates]: https://www.datocms.com/product-updates/starter-kits-now-ship-with-a-plugin-scaffold.md

[date: 2026-04-09T11:56:22.368+02:00]

The Astro and Next.js starter kits now come with a private DatoCMS plugin already wired up, giving you a ready-made foundation to start building custom editor experiences from day one, with no separate repo, and no manual installation.

Because the **Plugin SDK** and `datocms-react-ui` component library are built on React, this feature is available in the React-based starters (Astro and Next.js) but not in the SvelteKit or Nuxt starters.

### Build custom plugins right inside your project

The plugin lives alongside your site code and is served as a regular page (/private-datocms-plugin). On your first deploy it installs itself automatically via the post-deploy hook — zero manual setup in the dashboard. The source mirrors the structure of official DatoCMS plugins.

(Image content)

The included config screen links straight to the docs to help you get started:

-   [Plugin SDK docs](https://www.datocms.com/docs/plugin-sdk/introduction.md)
-   [Build your first plugin guide](https://www.datocms.com/docs/plugin-sdk/build-your-first-plugin.md)
    
-   [Hooks overview](https://www.datocms.com/docs/plugin-sdk/what-hooks-are.md)
    

### Astro starter upgraded to Astro 6

The Astro starter has also been upgraded to Astro 6 along with all official integrations (@astrojs/node, @astrojs/react, @astrojs/check) and @datocms/cli v4. The minimum Node version is now 22.

---

# Automatic antivirus scanning for all Media Area uploads

Source [product-updates]: https://www.datocms.com/product-updates/automatic-antivirus-scanning-for-all-media-area-uploads.md

[date: 2026-04-07T18:14:16.415+02:00]

Every file uploaded to the DatoCMS Media Area is now automatically scanned for viruses and malware. No configuration, no opt-in or workflow changes required from your side.

### How it works

(Video content)

The moment a file is uploaded, a background scan is queued automatically. Editors can continue working since there's no blocking step or wait time. Within seconds, each file is assigned one of four statuses:

-   **Clean**: no threats detected and file is served normally
-   **Infected**: a threat was detected and the file is automatically quarantined
    
-   **Skipped:** the file exceeds the scanner's size or type limits and could not be assessed. Treat these files with appropriate caution
-   **Failed**: an error occurred during scanning, and will be retried automatically up to 6 times with exponential backoff. If it still cannot be scanned, then the file remains in a failed state
    

ℹ️ If an asset is replaced with a new version, the antivirus scan runs again automatically on the new file.

### Quarantined files

When a threat is detected, infected files are automatically quarantined and DatoCMS will:

1.  Remove the file from public storage
    
2.  Purge it from the CDN cache, and
    
3.  Keep the upload record visible in the Media Area so editors can see it was flagged, but the file URL will no longer serve any content
    

Editors should replace the asset to restore functionality.

For **projects using a custom storage bucket**, DatoCMS does not have permission to delete or move files from your storage. In this case, the file will still be accessible from your own bucket even after being flagged, and the upload record will be marked as infected, and editors will see the file path so they can remove it manually. The warning UI in the Media Area will reflect this.

### Dashboard Changes

Infected files are surfaced throughout the Media Area:

A **"Threat detected"** badge appears on the upload card in grid, masonry, and table views. On smaller cards, this collapses to an icon with a tooltip

(Image content)

Opening an infected file replaces the normal preview with a **warning screen** that explains the situation, shows the specific threat name (useful for investigation), and prompts the editor to replace the asset

(Image content)

For custom storage projects, the warning is adjusted to show the file path and advise manual removal from the bucket

Editors can filter uploads by antivirus status (clean, infected, skipped, failed, pending) directly in the Media Area search. This filter is **not available** in the Content Delivery API.

Scan results are delivered in real time with the antivirus status in the dashboard updating live without requiring a page refresh, and Webhooks are fired on status changes, so you can build integrations that react to scan results, for example, getting a Slack alert when an infected file is detected in your project.

### API Access

The antivirus status is also available on every upload object via the CMA, under a new `meta.antivirus` field:

```json
"meta": {
  "antivirus": {
    "status": "infected",
    "scanned_at": "2026-03-27T18:51:00Z",
    "threat_name": "Trojan.GenericKD.12345"
  }
}
```

The object includes the scan status, the timestamp of the last scan, and the threat name when applicable. It's worth knowing that antivirus scan results are preserved when forking environments, with no rescanning needed, and when duplicating a project, infected files are automatically excluded from the copy to prevent propagation.

[Refer to the docs](https://www.datocms.com/docs/general-concepts/media-area.md#antivirus-scanning) for more information on how this works.

---

# Configurable \`hue\` property on Visual Editing

Source [product-updates]: https://www.datocms.com/product-updates/configurable-hue-property-on-visual-editing.md

[date: 2026-03-23T13:30:59.163+01:00]

When using the Visual Editing feature, the overlay color used to highlight editable areas can now be customized via a `hue` property (accepts values 0–359) in the configuration.

Previously, editable regions were always indicated with an orange highlight, which could create a usability problem for sites with orange-heavy designs, where the overlay could blend into the page.

(Image content)

All the SDKs now have a configurable `hue` property to allow the highlight to stand out regardless of your brand palette.

(Image content)

Get started with [Visual Editing from the docs](https://www.datocms.com/docs/visual-editing.md).

---

# Introducing Permissions for Asset Collections

Source [product-updates]: https://www.datocms.com/product-updates/introducing-permissions-for-asset-collections.md

[date: 2026-03-10T13:50:05.408+01:00]

Building up on our earlier release for Asset Collections, you can now assign specific permissions for users to assets within Collections, including read, write, and a new dedicated `move` permission that controls whether users can move assets from one collection to another.

Permissions assigned to a collection are automatically inherited by its sub-collections, with inheritance rules clearly displayed in the UI.

(Video content)

To enable or change access for collections, go to settings and select Asset Permissions \> Collections when editing User Roles.

(Image content)

---

# Pre-filter linked records with saved filters

Source [product-updates]: https://www.datocms.com/product-updates/pre-filter-linked-records-with-saved-filters.md

[date: 2026-02-18T10:29:06.258+01:00]

If you've ever had editors accidentally linking the wrong records because the list was too broad, this one's for you. When linking records, the list of available items can get overwhelming, especially when it includes outdated or irrelevant records that editors shouldn't pick (like records still in drafts).

To address this, we've introduced a way to pre-filter linked records with saved filters, letting you control exactly which records appear when editors browse items to link.

**What's new?**

The standard editors for Single and Multiple Link fields (compact and expanded) now include a newsettings section. Here, you can pair each linked model with one of your saved shared filters, so editors only see the records that they should.

**How do I enable it?**

Head to the **Presentation** tab in your Link field settings. You'll see the new Applied Filters section right away: just pick a model, choose a filter, and you're done. If you need more filters, create more saved filters in the Content Area to configure another.

(Video content)

The only prerequisite is that you need at least one **shared** saved filter for the models you want to filter. Private filters aren't available here.

---

# New media editor for images AND videos

Source [product-updates]: https://www.datocms.com/product-updates/new-media-editor-for-images-and-videos.md

[date: 2026-02-13T16:25:45.200+01:00]

We've rolled out considerable changes and updates to the media editor inside the CMS's Media Area. The new update brings changes to editing images, and introduces in-CMS video editing.

If you need to edit an uploaded image, you can use the built-in editor to crop, rotate, apply predefined color filters, tweak colors, and resize images:

(Video content)

Similarly, if you need to edit an uploaded video, you can also do this in the CMS now, with options to trim, add filters, and other similar edits.

(Video content)

After you're done editing your asset, you can choose to save it as a duplicate (available under a new URL), or replace the original asset and retain the URL.

---

# Introducing Visual Editing

Source [product-updates]: https://www.datocms.com/product-updates/introducing-visual-editing.md

[date: 2026-02-10T10:31:24.746+01:00]

We've rolled out big changes to how editors and content creators can interact with content in DatoCMS. Instead of navigating through forms and fields in the CMS interface, editors can now see their content exactly as it appears on the live site, click directly on any element to edit it, and watch changes appear instantly.

Visual Editing lets your content editors click directly on any element of your website and edit it in DatoCMS — no more hunting through record forms, switching tabs, or guessing which field maps to which headline. Available on every plan, including Free.

Visual Editing supports two workflows. Use either one, or both depending on which approach suits your content editors best.

### Click-to-edit: Content Link on your website

This is the simplest setup. Editors visit your website in draft mode, hover over content to see what's editable, and click to open DatoCMS in a new tab. It works entirely on your frontend.

(Video content)

This also works entirely on your website, no DatoCMS plugin required. It's a great starting point that already provides significant value to editors.

### Visual Mode: Side-by-side editing in the CMS

We made major updates to the Web Preview plugin to give editors the ideal setup: preview on the left, edit panel on the right, click anything, edit immediately, see it update live.

When they click on content, the edit panel opens instantly in the same view with no tab switching required.

(Video content)

This plugin also enables you to have preview links in the CMS sidebar, have bidirectional navigation (scroll either panel, the other panel will keep up with context), and give you full-screen Visual Editing mode.

## Getting started

We're making it as easy as possible for you to get started with Visual Editing.

We have dedicated SDKs and in-depth integration guides for [React/Next.js](https://www.datocms.com/docs/next-js/visual-editing.md), [Astro](https://www.datocms.com/docs/astro/visual-editing.md), [Svelte/SvelteKit](https://www.datocms.com/docs/svelte/visual-editing.md), and [Vue/Nuxt](https://www-draft.datocms.com/docs/nuxt/visual-editing). Each provides its own `<ContentLink />` component (or equivalent) that handles detection, overlay rendering, and keyboard shortcuts. Drop it into your layout, and you're done.

Want to see them in action first? Clone one of our [starter kits](https://www.datocms.com/marketplace/starters.md) — they come pre-configured with Draft Mode, Real-time Updates, Content Link, and Web Previews already wired together!

---

# Separate controls for record links and inline records in Structured Text fields

Source [product-updates]: https://www.datocms.com/product-updates/separate-controls-for-record-links-and-inline-records-in-structured-text-fields.md

[date: 2026-01-28T12:33:19.853+01:00]

You can now choose whether editors can link to records, embed them inline, or both, independently controlling each option in your Structured Text fields.

### The problem

Structured Text fields let editors reference other records in two ways: as clickable links or as embedded inline content. Until now, these were bundled together. If you enabled record references, editors got both options in the menu, even if you only wanted one. This meant developers had to handle two different content structures in their code when they only needed one.

### What's new?

The **Presentation** tab in your field settings now includes separate toggles for **Link to record** and **Inline record**, alongside other editor features like Blockquote and Heading.

(Image content)

This gives you cleaner content structures. If you only want editors to link to products without embedding them inline, just disable Inline record. Your GraphQL queries and rendering logic only need to handle the shape you actually use.

### Compatibility

All existing Structured Text fields will have both options enabled by default, so everything continues working exactly as before. To change the behavior for a field, open its settings and adjust the toggles in the Presentation tab.

---

# Include milliseconds in date serialization

Source [product-updates]: https://www.datocms.com/product-updates/milliseconds-in-datetime.md

[date: 2026-01-16T16:40:48.894+01:00]

We've added an opt-in configuration option to include milliseconds in date serialization across both the Content Delivery API (CDA) and Content Management API (CMA).

### The problem

Until now, DatoCMS APIs serialized dates without milliseconds (e.g., `2025-12-05T00:01:00-05:00`), even though the underlying data stores full millisecond precision.

When customers sort records by date, items with the same timestamp (when rounded to seconds) appear out of order. This looks like a sorting bug, but it's actually correct behavior—the records have different millisecond values that simply aren't visible in the API response.

### What's new

You can now opt-in to include milliseconds in date serialization. Once enabled, both APIs return full precision:

**GraphQL CDA:**

```graphql
{
  allBlogPosts(orderBy: createdAt_DESC) {
    createdAt  # Returns "2025-12-05T14:30:00.345+00:00"
  }
}
```

**REST CMA:**

```json
{
  "data": {
    "attributes": {
      "created_at": "2025-12-05T14:30:00.345+00:00"
    }
  }
}
```

This allows you to see the full precision of date fields, makes sorting behavior transparent and predictable, and ensures consistent date formats across both APIs.

### How to enable it

The change is opt-in at the project level to ensure backward compatibility. Navigate to **Configuration / Available Updates** and enable the "Include milliseconds in date serialization" option.

---

# Replace assets without breaking existing URLs

Source [product-updates]: https://www.datocms.com/product-updates/replace-assets-and-retain-urls.md

[date: 2026-01-13T18:46:50.680+01:00]

This has been one of our most requested features, and it’s finally here: you can now replace assets in your DatoCMS project while keeping the original URL.

When replacing an asset, you now have two options:

-   **Create new URL** (existing behavior): The new asset is available immediately, but with a fresh URL. The old URL will be purged from cache and will disappear after complete propagation. Use this when you need immediate availability of the new file.
-   **Keep the original URL (new!)**: Existing links will continue to work automatically, but changes may take 5-10 minutes to appear everywhere due to CDN and browser cache propagation. Some users may temporarily see the old version. Use this when you have many existing references and want to avoid updating URLs.
    

(Video content)

##### Limitations

Assets can only be replaced keeping the original URL if:

-   The new asset has the same file format as the original (for example, JPEG → JPEG).
-   You are using DatoCMS’s default asset manager. This feature is not supported when using [custom Enterprise asset storage](https://www.datocms.com/marketplace/enterprise.md) (S3/GCP).
    

For full details on the related API changes, see the [CMA documentation](https://www.datocms.com/docs/content-management-api/resources/upload/update.md#replace_asset).

---

# Improving the items listing in CMA

Source [product-updates]: https://www.datocms.com/product-updates/improved-items-listing.md

[date: 2026-01-07T13:15:47.000+01:00]

We're changing the default behavior when you [list all records](https://www.datocms.com/docs/content-management-api/resources/item/instances.md) in the Content Management API to better reflect modern content workflows. The default `version` parameter will change from `published` to `current`, giving you access to the latest version of your content by default.

### What's changing

When you list all records **without explicitly specifying** a `version` parameter:

-   **Previously:** The endpoint returned only published records (`version: "published"`).
-   **Now:** The endpoint will return the latest available version of each record (`version: "current"`), which may include drafts, updated content, or published records.
    

### Who is affected by this change?

This change will apply to all brand new DatoCMS projects created from today onwards. If you have an existing project that you'd like to update, you can manually opt-in to this behavior in the Environment Settings.  

Please note that this change cannot be undone, so **we strongly recommend testing the effects in a sandbox environment** before applying the change to your primary environment.

**Existing projects that do not opt-in will maintain the current default behavior (****`published`****).**

### **Why we're making this change**

This new default aligns better with common use cases:

-   **API consistency:** When you [retrieve a record](https://www.datocms.com/docs/content-management-api/resources/item/self.md), it already returns the latest version by default (`current`). This change makes listing all records behave consistently with retrieving a single record.
-   **Developer expectations:** This matches the behavior developers typically expect when fetching content programmatically.
    

### What you need to do

**For new projects created from today onwards:** If you want to receive only published content instead of the latest version, simply add the `version` parameter explicitly:

```javascript
const records = await client.items.list({
  version: "published", // Explicitly request only published records
  filter: {
    type: "blog_post"
  }
});
```

**For existing projects:** Your project will continue to use the current default (`published`) unless you manually opt-in to the new behavior via Environment Settings. We recommend testing the change in a sandbox environment first.

### More information

For complete details on the `version` parameter and how to filter by publication status, see our [List all records documentation](https://www.datocms.com/docs/content-management-api/resources/item/instances.md).  
If you have questions or concerns about this change, please [reach out to our support team](https://www.datocms.com/support.md).

---

# Reactive plugins

Source [product-updates]: https://www.datocms.com/product-updates/reactive-plugins.md

[date: 2025-12-04T18:23:39.701+01:00]

We've just rolled out a significant improvement to the plugin scope: as of today, **plugin settings are synced across multiple users in real time**, just as we do for records and other entities.

##### What's new

Until today, plugin data were loaded into DatoCMS at a few predefined moments: when opening the CMS, after installing a new plugin, and... that was pretty much it. That caused issues in some non-trivial cases involving plugins with complex parameters: if multiple users updated a plugin's settings at the same time, they risked overwriting each other's changes.

We solved the problem with the same approach with many other entities of DatoCMS: now the changes to plugin parameters are propagated to other users instantly, so that they always see the last version of it.

---

# See Last Used Time for API Tokens

Source [product-updates]: https://www.datocms.com/product-updates/see-last-used-time-for-api-tokens.md

[date: 2025-12-16T12:46:17.602+01:00]

Its now easier to understand the usage of API tokens associated with your project.

We've made their "Last Used Time" visible across your project settings, making it helpful for projects with many tokens to see each one's activity on the CMA and CDA.

If a token was last used months ago, its probably safe to get rid of them.

To see tokens and their usage, head over to Project Settings \> API Tokens and select the token you want to see the associated activity for.

(Image content)

(Image content)

---

# Smart confirmation guardrails

Source [product-updates]: https://www.datocms.com/product-updates/smart-confirmation-guardrails.md

[date: 2025-11-24T11:22:56.019+01:00]

We've just rolled out Smart Confirmation Guardrails to prevent accidental data loss while keeping your workflow smooth and efficient.

### What's New?

Destructive actions now show clear consequences before you commit. No more "oops" moments.

In primary environments, high-risk changes affecting 10+ records require explicit confirmation by typing what you're destroying (e.g., "delete author\_name field in 2k+ records"), while smaller changes use lighter confirmations to keep your workflow fast. Sandboxes remain unrestricted for rapid iteration.

### How Does It Work?

When you attempt a destructive action in your primary environment, the system calculates potential impact.

(Video content)

If 10+ records will be affected, it shows a detailed confirmation dialog with exactly what will be deleted or modified, how many records will be impacted, and a confirmation phrase to type that reinforces the consequences.

(Video content)

If fewer than 10 records will be affected, a simpler dialog will be shown that doesn't require a confirmation phrase.

### Why Does It Matter?

Accidentally deleting a model, converting a field type, or removing a locale can cause massive data loss, broken references, and hours of recovery work. These guardrails give you **safety without friction** — protection appears exactly when and where you need it most.

This is especially valuable for:

-   **Enterprise teams** managing large-scale content operations with thousands of records
-   **Multi-locale projects** where removing a language could affect hundreds of assets and records
    
-   **Complex schemas** with interconnected models and references
    

You get enterprise-ready protection with smart defaults: your production environment stays safe, while development environments stay fast.

---

# Build triggers and Site Search are now two different entities

Source [product-updates]: https://www.datocms.com/product-updates/build-triggers-and-site-search-are-now-two-different-entities.md

[date: 2025-12-03T16:37:17.447+01:00]

We've just rolled major changes to the Site Search feature: settings for how to index websites now have a dedicated section in the Project settings, while build triggers have become leaner.

### What's new

Until today, Site Search was buried deep within build triggers and tightly integrated with the build system: you could crawl a website only if you had a build trigger.

That wasn't always what you needed. Maybe on a non-static website, there's no build phase at all, but you rightly want to leverage the Site Search functions.

So we improved it to bring about a few major changes:

-   **Site Search is now independent of Build Triggers being used.** to benefit from Site Search, you simply have to create a Search Index, and you're good to go. No more artificial coupling.
-   **Site Search logs split apart from Build Triggers logs.** Build triggers and indexing logs are now two different sections in your Project settings.
    
-   **Independent control over indexing.** You can control when and how your sites are indexed for search purposes independently of their build process (even if you don't have one).
    

ℹ️ If you had Site Search enabled, your setup has been migrated to the new configuration: for each build trigger with indexing enabled, we created a search index and linked it to the build trigger.

### How does it work?

Navigate to the **Search Indexes** section in Project settings to create or manage your search indexes:

(Video content)

### Why the change?

Decoupling search indexes from build triggers gives customers explicit control over when indexing happens and provides independent failure reporting and logging for indexing.

Plus, it opens up new possibilities: you can now add a custom suffix to the user-agent we use when crawling websites. With that, you can work on your robots.txt and sitemaps to get different search indexes for various sections of your website (for example, one for the blog, another for the documentation, and another for the FAQs).

---

# Filter uploads by path

Source [product-updates]: https://www.datocms.com/product-updates/filter-uploads-by-path.md

[date: 2025-11-17T18:47:31.345+01:00]

We've added a new **path filter** for uploads in the GraphQL API (CDA), allowing you to filter uploads based on their storage path.

The filter supports exact matching (`eq`), negation (`neq`), inclusion (`in`), and exclusion (`notIn`) operations. All path comparisons are case-insensitive.

```graphql
{
  # Find a specific asset by path
  upload(filter: { path: { eq: "/123/1763384420-hero-image.jpg" } }) {
    id
    url
  }

  # Find multiple assets by paths
  allUploads(filter: { path: { in: ["/123/1763382120-logo.png", "/123/1763381111-banner.jpg"] } }) {
    id
    url
  }

  # Exclude specific paths
  allUploads(filter: { path: { notIn: ["/123/1763383331-preview.jpg"] } }) {
    id
    url
  }
}
```

You can also use this new filter when [fetching uploads](https://www.datocms.com/docs/content-management-api/resources/upload/instances.md) via the CMA or in your project Media Area

(Image content)

---

# Favorite Locales: Streamline Multilingual Editing

Source [product-updates]: https://www.datocms.com/product-updates/favorite-locales-streamline-multilingual-editing.md

[date: 2025-11-04T15:13:53.126+01:00]

We're introducing **favorite locales** to keep your workspace focused and your most-used languages instantly accessible.

**What's New?**

Mark up to 6 locales as favorites to prioritize them across the CMS, reducing clutter and eliminating the need to hunt through long language lists.

-   **Instant access**: Favorite locales appear at the top of selectors throughout the CMS — no more hunting through lists.
-   **Cleaner workspace**: While editing records, collapse non-favorite locales to keep your view focused on what matters.
    

**How Does It Work?**

Open Editor's Preferences from your avatar menu (upper right corner), enable "Favorite Locales," and select your most-used languages. You can choose up to 6 favorite locales.

(Video content)

**Why Does It Matter?**

Managing 10, 15, or 20+ locales means constant scrolling and visual noise. By surfacing only the languages you use regularly, this feature eliminates friction in your daily workflow — especially valuable when you work primarily in a consistent subset of locales. You get faster navigation and a cleaner interface without changing how localization works.

---

# Access to CDA Playground with limited permissions

Source [product-updates]: https://www.datocms.com/product-updates/access-to-cda-playground-with-limited-permissions.md

[date: 2025-11-10T17:49:52.366+01:00]

We're adding a new way to let users access the CDA Playground without requiring "Create/edit API tokens" permissions in their role.

This is especially useful for developers — including external contractors — who have been given an API token by someone else, and need to explore and test content queries but don't require full API token management capabilities.

#### What's New

For people with no permission to "Create/edit API tokens", we've added a **"Force CDA playground visibility"** toggle in the **Editor preferences**. When enabled, users can access the CDA Playground regardless of their role permissions.

(Video content)

#### How It Works

For users with this toggle enabled, the CDA Playground will prompt for an API token on first access.

(Image content)

Once a valid token is provided, the playground functions normally — the toolbar displays the custom token and allows updating it as needed.

(Image content)

This gives developers and technical users the access they need to work with the Content Delivery API, without granting broader API token management permissions.

---

# No new Travis CI and CircleCI build triggers

Source [product-updates]: https://www.datocms.com/product-updates/no-new-travis-ci-and-circleci-build-triggers.md

[date: 2025-11-03T17:39:18.736+01:00]

We recently removed the ability to add new Travis CI and CircleCI build triggers: these integrations have been practically unused for years. Maintaining them creates technical debt and maintenance burden without providing value to our users.  
  
Existing Travis CI and CircleCI build triggers will be migrated to custom build triggers in the following weeks. After the migration is complete, the migrated **CircleCI triggers** will be fully functional — both outbound triggers and incoming webhooks continue to work seamlessly. For **Travis CI triggers**, that have not been used in the last 6 years, we'll continue to manage outbound triggers, which will still work, while incoming webhooks won't be recognised as valid.

We'll contact all paying customers with existing Travis CI and CircleCI build triggers via email to inform them about the incoming changes.

---

# LLM-Ready Documentation: export any page as Markdown!

Source [product-updates]: https://www.datocms.com/product-updates/llm-ready-documentation-export-any-page-as-markdown.md

[date: 2025-10-15T15:28:32.614+02:00]

We've added **native** [**LLM.txt**](https://llmstxt.org/) **support** to make our documentation and blog instantly accessible to AI assistants like ChatGPT and Claude.

Every documentation and blog page now includes a "Copy page" dropdown that lets you:

-   **Copy the page as Markdown** \- Get clean, formatted Markdown optimized for LLMs
-   **Copy a shareable .md link** \- Direct link to the Markdown version
    
-   **Open in ChatGPT** \- Launch ChatGPT with the page pre-loaded
-   **Open in Claude** \- Launch Claude with the page pre-loaded
    

(Image content)

#### **Why It Matters**

When working with AI assistants, feeding them accurate documentation context is critical. Instead of copy-pasting HTML or having the AI scrape pages (which often loses structure), you can now give your AI assistant perfectly formatted documentation in one click.

Whether you're debugging an integration, exploring our APIs, or asking questions about DatoCMS features, your AI assistant now has direct access to our complete documentation in the format it works best with.

**P.S.** You can actually append `.md` to any page URL on our site to get its Markdown version — though we can't guarantee the quality of the conversion for all pages yet!

---

# Introducing datocms/structured-text-to-markdown

Source [product-updates]: https://www.datocms.com/product-updates/introducing-datocms-structured-text-to-markdown.md

[date: 2025-10-02T09:20:51.987+02:00]

You can now turn Structured Text fields back into Markdown with our new [`datocms/structured-text-to-markdown`](https://github.com/datocms/structured-text/tree/main/packages/to-markdown) package. Perfect if you need clean Markdown output for your pipelines, exports, or tooling (or just a clean .md page for all the LLMs 🤭).

Just a lil

Terminal window

```bash
npm install datocms/structured-text-to-markdown
```

and you're set!

The renderer supports all DatoCMS Structured Text nodes and converts them to CommonMark-compatible Markdown:

**Block Nodes**

-   **Headings**: `# H1` through `###### H6`
-   **Paragraphs**: Plain text with double newlines
    
-   **Lists**: Both unordered (`-`) and ordered (`1.`) lists with nested support
-   **Blockquotes**: Lines prefixed with `>`
    
-   **Code blocks**: Fenced code blocks with language support
-   **Thematic breaks**: Horizontal rules (`---`)
    

**Inline Formatting**

-   **Strong**: `**bold**`
-   **Emphasis**: `*italic*`
    
-   **Code**: `` `code` ``
-   **Strikethrough**: `~~text~~`
    
-   **Highlight**: `==text==` (extended Markdown)
-   **Underline**: `<u>text</u>` (HTML fallback, no native Markdown)
    

**Links**

-   **Regular links**: `[text](url)`
-   **Record links**: Custom rendering via `renderLinkToRecord`
    

Full deets on the package and advanced usage are [on the README](https://github.com/datocms/structured-text/blob/main/packages/to-markdown/README.md).

---

# Improved Link Field Filtering By Locale

Source [product-updates]: https://www.datocms.com/product-updates/improved-link-field-filtering-by-locale.md

[date: 2025-09-03T09:45:32.721+02:00]

We’ve fixed a long-standing issue with **localized Link fields**.

When editors used a Link field to reference entries, the dropdown showed **records from all locales**, even if those records were not available in the locale currently being edited. This often caused confusion in the UI, query errors and other unwanted production quirks.

Now, the Link field dropdown only shows **records available in the current locale**. This change should make managing localized content clearer and safer out of the box.

---

# Single Block fields now can be used as presentation title or image

Source [product-updates]: https://www.datocms.com/product-updates/single-block-fields-presentation-title-or-image.md

[date: 2025-08-27T20:58:33.483+02:00]

Many of you use shared **Single Block fields** to store common elements like title and image across models. Until now, these couldn’t be used as a model’s Presentation Title or Image — now you can!  
You can now select a Single Block field in the **Presentation tab**, and DatoCMS will use the **title** and **image** set inside the chosen block as the model’s Presentation Title and Presentation Image.

### How to use it

1.  Go to **Edit Model → Presentation tab**
    
2.  Select your Single Block field as *Presentation Title* or *Presentation Image*
    
3.  Remember to set the presentation title and image you prefer in your Single Block’s own Presentation settings.

---

# All API tokens are now fully deletable

Source [product-updates]: https://www.datocms.com/product-updates/all-api-tokens-are-deletable-now.md

[date: 2025-08-27T20:59:04.524+02:00]

From now on, **all API tokens** — including the default non-editable tokens that are automatically created by DatoCMS — **can be deleted**.

Previously, default non-editable tokens (such as the read-only token) could not be removed. This change is a matter of security, giving you full control over your API tokens and allowing you to remove any unused or potentially exposed credentials.

💡 **Reminder:**  
If you delete a default non-editable token generated automatically, it cannot be recreated. However, you can always create a new one with the same or custom permissions.

---

# Trees also grow on DatoCMS

Source [product-updates]: https://www.datocms.com/product-updates/trees-also-grow-on-datocms.md

[date: 2025-07-25T10:06:07.811+02:00]

DatoCMS has long let editors organize records hierarchically. However, the feature was missing some elements to be considered as complete.

We covered the missing parts 💁‍♀️

(Video content)

-   From today, a new "Tabular View" for trees of records is available - aside from the "Compact View", giving a bit more consistency on how records are visible across all model types. It brings all the freedom in playing with columns you know from the regular item list to hierarchically organized records too, without losing out on the drag-and-drop fun.
-   We also removed the limitation to the number of records that can be organized in a tree: both the tabular and the compact view are now paginated, and records are incrementally shown as they are loaded.

---

# We no longer create a default full-access API token for new projects

Source [product-updates]: https://www.datocms.com/product-updates/we-no-longer-create-a-default-full-access-api-token-for-new-projects.md

[date: 2025-05-05T10:40:45.876+02:00]

To promote safer and more intentional permission management, new DatoCMS projects will no longer generate a default full-access API token (i.e. one with both read and write permissions for all APIs).

Previously, new projects came with two tokens by default: a full-access token and a read-only token. From now on, only the **read-only token** will be created automatically. This change encourages users to explicitly define the scope of each token based on their specific needs.

🔒 **Reminder:**  
Existing projects still have this legacy token. If you’re using it, we strongly recommend ensuring it’s used only in trusted environments.

---

# Legacy batch uploads operations endpoints will be sunset on May 14th

Source [product-updates]: https://www.datocms.com/product-updates/deprecated-batch-uploads-operations-endpoints-will-stop-working.md

[date: 2025-04-14T09:20:36.652+02:00]

Starting May 14th, 2025, our legacy uploads batch operation endpoints — which have been deprecated for years — will be discontinued and will return error responses. Documentation for these endpoints can be found here:

-   [https://www.datocms.com/docs/content-management-api/resources/upload/batch\_destroy](https://www.datocms.com/docs/content-management-api/resources/upload/batch_destroy.md)
-   [https://www.datocms.com/docs/content-management-api/resources/upload/batch\_add\_tags](https://www.datocms.com/docs/content-management-api/resources/upload/batch_add_tags.md)
    

### Required action

If you're using any of these endpoints, please update your integration to use their replacements. Documentation for the current endpoints can be found here:

-   [https://www.datocms.com/docs/content-management-api/resources/upload/bulk\_destroy](https://www.datocms.com/docs/content-management-api/resources/upload/bulk_destroy.md)
-   [https://www.datocms.com/docs/content-management-api/resources/upload/bulk\_tag](https://www.datocms.com/docs/content-management-api/resources/upload/bulk_tag.md)
    

### Impact assessment

Based on our monitoring over the last 30 days, no projects are actively using these deprecated endpoints.

---

# Fixed Headers for a more consistent UX

Source [product-updates]: https://www.datocms.com/product-updates/a-more-consistent-cms-experience-with-fixed-headers.md

[date: 2025-04-08T12:34:44.380+02:00]

We’re introducing **a unified fixed header across all CMS sections**, ensuring a smoother, more predictable interface.

### **What’s New?**

We’ve standardized the header behavior so that key navigation elements stay accessible at all times—no matter where you are in the CMS.

-   **Fixed headers everywhere** – The header now stays in place as you scroll.
-   **Consistent UI patterns** – The same styling, spacing, and interaction logic apply across all sections, reducing cognitive load.
    
-   **Quick access to actions** – Important buttons (like *Save*, *Publish*) remain within reach without unnecessary scrolling.
    

### **How Does It Work?**

No setup is needed—just log in, and you’ll see the updated interface immediately!

### **Why Does It Matter?**

This update eliminates inconsistencies, making the CMS more intuitive and reducing friction in your daily tasks. If you’ve ever lost time scrolling back up to access a button or felt disoriented by shifting layouts, this change will make navigation feel effortless.

---

# Introducing DatoCMS Recipes

Source [product-updates]: https://www.datocms.com/product-updates/introducing-datocms-recipes.md

[date: 2025-03-26T15:32:42.095+01:00]

With the release of the recent plugin its now possible to export JSON files of your models and blocks, and easily import them into another DatoCMS project.

To make it easier to get started with new projects and build out common schema models, we've released [DatoCMS Recipes](https://www.datocms.com/marketplace/recipes.md) in the marketplace — a curated selection of ready-to-use models and blocks that you can import into your projects.

Simply choose a recipe, click install, choose which project you'd like it in, and we'll import the corresponding model with all blocks, relations, and plugins as required.

(Video content)

Want to create your own recipes? Install the plugin to export models and reimport them into another DatoCMS project!

---

# Improving the exposure of Inline Blocks in the Content Delivery API

Source [product-updates]: https://www.datocms.com/product-updates/improved-exposure-of-inline-blocks-in-cda.md

[date: 2025-03-24T10:58:35.712+01:00]

Based on early feedback from the recent release of [inline blocks for Structured Text fields](https://www.datocms.com/product-updates/inline-blocks-are-landing-in-structured-texts.md), we're introducing a small but impactful update to how our **Content Delivery API** handles them.

Previously, **inline blocks** were included within the `blocks` GraphQL selector. With this update, they now have a dedicated `inlineBlocks` selector:

#### **Before the update:**

```graphql
query Before {
  blogPost {
    content {
      value
      blocks {
        __typename
        ... on BlockModelRecord {
          title
        }
        ... on InlineBlockModelRecord {
          title
        }
      }
    }
  }
}
```

#### **After the update:**

```graphql
query After {
  blogPost {
    content {
      value
      blocks {
        __typename
        ... on BlockModelRecord {
          title
        }
      }
      inlineBlocks {
        __typename
        ... on InlineBlockModelRecord {
          title
        }
      }
    }
  }
}
```

### **Why this change?**

By separating `inlineBlocks` from `blocks`, developers gain more precise control over **rich text content**, making it easier to manage and render structured text accurately, especially when using TypeScript. This improvement enhances both flexibility and consistency when working with inline blocks in GraphQL queries.

We appreciate your feedback and will continue refining the experience to make content management even smoother! 🚀

---

# Introducing Automatic Data Export Options \[Enterprise Feature\]

Source [product-updates]: https://www.datocms.com/product-updates/introducing-automatic-data-export-options-enterprise-feature.md

[date: 2025-03-05T15:20:57.696+01:00]

### For Enterprise Customers

We've recently rolled out the ability for enterprise customers to schedule frequent exports of their DatoCMS data to AWS S3 buckets.

While it's important to note that this is **not an automatic backup & restore function**, the Project Export feature allows Enterprise customers to export all content and assets from their DatoCMS project to their own AWS S3 bucket.

Here's some important considerations:

-   **Not a Backup Solution**: The export does not offer a one-click restoration process.
-   **Primary Environment Only**: Only the primary project environment is included in the export.
    
-   **Automated and Scheduled**: Exports occur on a predefined schedule, with a minimum frequency of once per month and a maximum of once per day.
-   **AWS S3 Storage Required**: Customers must configure their own S3 bucket to receive the exported data.
    

The exported data includes:

-   **Schema Models**: Fields and fieldsets.
-   **Schema Blocks**: Block definitions and fields.
    
-   **Records**: Current and published versions, including block records.
-   **Uploads**: Metadata and references for uploaded assets.
    
-   **Project Settings**: Locales, SEO settings, workflows, and installed plugins.
-   **Asset Files**: All uploaded files from the media area.
    

To look into how to use this,[**refer to the docs**](https://www.datocms.com/docs/import-and-export/datocms-site-export-feature.md).

### For All Users

That being said, there's several existing methods for all users to ensure data integrity with some recommended DIY approaches to data exports, backups, and recovery options.

These include several options including Plugins, Scripts, and Environment clones. Refer to our docs on [**Available Export & Backup Options**](https://www.datocms.com/docs/import-and-export/export-data.md) for more info.

---

# Improved Build Triggers Activity view

Source [product-updates]: https://www.datocms.com/product-updates/improved-build-triggers-activity-view.md

[date: 2025-03-04T16:18:27.089+01:00]

Similar to our [recent updates in the Webhooks Activity section](https://www.datocms.com/product-updates/improvements-to-handling-webhooks.md), we have also enhanced the Build Triggers Activity view.

Previously, the CMS displayed only the 30 most recent build events.

We have now improved this by providing options to view events from the past weeks, and to apply filters or custom ordering, while also updating the UI to align with the overall design of the CMS.

(Image content)

---

# Enhanced previews: Sneak peeks into blocks and records

Source [product-updates]: https://www.datocms.com/product-updates/enhanced-previews-quickly-see-what-s-inside-your-blocks-and-links.md

[date: 2025-02-28T15:25:26.643+01:00]

We’ve heard from many of you that it’s tough to quickly grasp what’s inside each record or block at a glance when shown in preview mode —especially when those blocks or records lack text fields.

To address this, we’ve introduced a new approach to displaying previews across the entire CMS: inline blocks, link fields, and even the item index, all benefit from clearer at-a-glance content!

**What's new?**

First, we’ve aligned how the “Presentation” tab works for both models and blocks, letting you pick a Title preview field and an Image preview.  
And second, we introduced new field types that can be selected as title and/or preview fields:

-   `Color` fields show a handy swatch (with optional opacity);
-   `Date`/`DateTime` fields use a quick calendar icon and display the date itself;
    
-   `Geolocation` fields bring in a map preview;
-   `Number` fields can be used as title and show their integer or floating-point value.
    

**How do I enable it?**  
Just head to the “Presentation” tab in your model or block settings to choose your Title and Image preview fields. No extra steps required!

(Image content)

**See it in action 👇**

(Video content)

**`Color`****,** **`Geolocation`****, and** **`Date`** **fields will also provide a much cleaner interface when set as Preview Fields.**

(Image content)

**Why does it matter?**

This update makes it faster and clearer to see what’s inside each record or block at a glance. If you’ve ever found yourself repeatedly opening blocks just to check their contents, you’ll love these new previews!

---

# Inline blocks are landing in structured texts!

Source [product-updates]: https://www.datocms.com/product-updates/inline-blocks-are-landing-in-structured-texts.md

[date: 2025-02-28T15:25:39.653+01:00]

For quite some time now, you've asked for the ability to insert blocks as inline elements inside the Structured Text field. Actually, it's [one of the most requested features](https://community.datocms.com/t/structured-text-inline-block/1821/8) on our forum.

Well, here we are 🎉

It's now possible to setup structured text fields and declare which blocks can be added as inline elements in the content.

**What’s new?**  
Stuctured text fields have a new setting that allows to select the types of block that can be used as inline element: that's very similar to what we already have for regular, stacked blocks.

**Why does it matter?**  
Inline blocks open infinite possibilities: from links with custom data to mentions, from hashtags to inline notes... Fantasy is the limit!

And that's just the beginning: Inline blocks can ALSO have structured text fields with inline blocks within: so you can nest blocks 🤯 (with the same limits you have on your plan for stacked blocks).

**How do I enable it?**  
Simply head over to any structured text field’s settings, locate the new "Allow adding Inline Blocks" in the “Validation” tab, and select the blocks you want to make available in the field.

(Image content)

**API Changes**

From the API perspective, the Dast changed a bit and it now supports inline blocks inside paragraphs and headers. You'll appreciate the difference, of course, only when you start using inline blocks: there's no impact on fields that keep using regular, stacked blocks.

Check out inline blocks in action 👇

(Video content)

---

# New dedicated fallback options for SEO per model

Source [product-updates]: https://www.datocms.com/product-updates/fine-tune-your-seo-with-new-dedicated-fallback-options.md

[date: 2025-02-28T15:25:34.779+01:00]

We’ve been carrying around an issue for quite some time regarding how we manage SEO fallback data. Because the same fields used for internal previews—Title and Image Preview—are also used for SEO fallback data, it’s been causing confusion and often results in less-than-ideal search engine metadata.

To address this, we’re splitting the two settings and creating a dedicated “SEO Fallback” tab!

**What’s new?**  
We’re removing the overlap between “Presentation” fields and “SEO” fields. Now you can explicitly choose which fields feed into your SEO Title, Image, and Description. These are separate from the fields used for internal previews, giving you more granular control over how your content appears in search results.

**Why does it matter?**  
With dedicated SEO fields, you can optimize exactly how content appears on search engines—without affecting the quick, at-a-glance previews in the CMS. This ensures your internal workflow remains clear and uncluttered, while your SEO metadata stays fine-tuned for external audiences.

**How do I enable it?**  
Simply head over to any Model’s settings, locate the new “SEO” tab, and select the fields you want to use for SEO Title, Image, and Description.

(Video content)

If you want to experiment with [all the new presentation fields](https://www.datocms.com/product-updates/enhanced-previews-quickly-see-what-s-inside-your-blocks-and-links.md) available, explore the head over the Presentation Tab!

###### ⚠️ API Changes

We've introduced two new relationships on the [`item_type` entity](https://www.datocms.com/docs/content-management-api/resources/item-type.md): `presentation_title_field` and `presentation_image_field`. These relationships are exclusively used for DatoCMS’s internal interface and preview functionality:

-   `presentation_title_field` supports fields of type: `string`, `date`, `date_time`, `link`, `text`, `structured_text`, `color`, `lat_lon`, `integer`, and `float`.
-   `presentation_image_field` supports fields of type: `file`, `gallery`, `link`, `color`, `date`, `date_time`, and `lat_lon`.
    

At the same time, the existing relationships — `title_field`, `image_preview_field`, and `excerpt_field` — are now dedicated solely to configuring SEO fallback settings. As a result:

-   `title_field` no longer accepts fields of type `date`, `date_time`, `link`, `text`, `structured_text`, or `color`. It now only supports `string` fields.
-   `image_preview_field` no longer accepts fields of type `link` or `color`. It now only supports `file` and `gallery` fields.
    

These changes only refine the accepted field types and do not affect functionality, as the previously accepted field types were already ignored for SEO purposes and if ie. a model had a `date` field set as `title_field` that field is now set as `presentation_title_field`.

---

# Improvements to managing user roles and content permissions

Source [product-updates]: https://www.datocms.com/product-updates/improvements-to-managing-user-roles-and-content-permissions.md

[date: 2025-01-27T16:29:19.317+01:00]

Handling complex user roles and permissions is complicated enough as it is, so we thought we'd try and simplify things on our end as much as possible!

Firstly, when creating highly specific and granular roles that are inherited from others, you'll see a cleaner and clearer indication of which permissions are inherited from parent roles.

(Video content)

While you're in the process of making any modifications or edits to them, we're also showing a new pill that explicitly highlights what's changing, so you can make sure everything's good before hitting save.

(Video content)

On the cosmetic side, we're also making it way easier to just *understand* the roles you're working with. Inside Content Permissions, all the rules are now shown as actual readable sentences instead of "disabled form fields" which are just so much more readable!

(Image content)

---

# New breakpoints and preview options for Web Previews

Source [product-updates]: https://www.datocms.com/product-updates/new-breakpoints-and-preview-options-for-web-previews.md

[date: 2025-01-23T13:37:20.694+01:00]

As one of the most used and installed plugins in the DatoCMS Marketplace, this update was long overdue.

You can now update this plugin to V1.0.24 which introduces custom breakpoints for desktop, mobile, and tablet, allowing your editors to preview Draft and Published content within the CMS for each device.

(Video content)

[Install the plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) via the Marketplace. If you already have it installed, you can head on to *Configurations \> Plugins* and click to Update the plugin to the latest version.

---

# Increased CDA GraphQL pagination response to 500 items

Source [product-updates]: https://www.datocms.com/product-updates/increased-pagination-response-to-500-items.md

[date: 2025-01-29T09:52:50.747+01:00]

By default, all GraphQL queries on paginated items to our Content Delivery API return a response of 20 items.

While we'll still retain this as the default behaviour, we've increased the maximum number of items to 500.

You can manage this behaviour when making your queries using the `first` parameter like this:

```graphql
{


# Fetch the first 500 posts
  allPosts(first: 500) {
    id
    slug
    title
    description
    publishedAt
    content
  }
}
```

For more info on working with Pagination in DatoCMS [check out the docs](https://www.datocms.com/docs/content-delivery-api/pagination.md#simplifying-pagination).

---

# Site Search Crawler Logs: Enhanced Visibility into Indexing Issues

Source [product-updates]: https://www.datocms.com/product-updates/site-search-crawler-logs-enhanced-visibility-into-indexing-issues.md

[date: 2025-01-14T11:16:01.045+01:00]

A new enhancement has just been released that should allow for easier investigation of any issues encountered with the indexing of pages by our [Site Search](https://www.datocms.com/docs/site-search.md) functionality.

By opening the details of events labeled **"Site spidering completed with success"**, it is now possible to access a detailed log of the operations performed by our crawler, including analysis of the robots.txt and any sitemaps.

(Image content)

At the end of the log, you will also find a handy list of pages that encountered indexing problems.

---

# Force validations on records when publishing

Source [product-updates]: https://www.datocms.com/product-updates/force-validations-on-records-when-publishing.md

[date: 2025-01-13T11:02:00.645+01:00]

Another one for content best practices! We're rolling out an option to stop CMS users from publishing invalid records.

How can published records be invalid you ask? Imagine records where `title` fields were required, and everything was published correctly. No sweat. You now extend that model to add in a new `subtitle` field which is also required. At this point, all the records in that model will require you to update them by filling in the `subtitle` field before their next publish.

While we'll enable this flag to TRUE for all new projects, you can easily enable it in your existing project under Configuration \> Available Updates.

---

# Draft mode active as default for models

Source [product-updates]: https://www.datocms.com/product-updates/activate-draft-mode-as-default.md

[date: 2025-01-10T12:40:14.598+01:00]

We’ve made a small but significant change to the way new models are created.

To enforce editorial best practices and provide better flexibility for your content workflows, **draft mode will now be enabled for all new projects**. While this is the new default, you are able to override this when creating new models, under the model's validation settings.

(Image content)

Additionally, for existing projects, we’ve added an option to enable this setting. You can find it in *Configuration \> Available Updates.*

(Video content)

---

# Save invalid drafts

Source [product-updates]: https://www.datocms.com/product-updates/draft-saving-has-landed.md

[date: 2025-01-08T12:37:34.297+01:00]

It is now possible to save invalid records in their draft state, and postpone validation enforcements to publication time. The feature can be enabled and/or disabled on models with the draft/published stages active.

The feature affects the CMS and, of course, the CMA (Content Management API). When draft saving is active, it's possible to POST/PUT invalid records to CMA and have them saved: the endpoints respond with a 200, and the record is just saved as a payload.

(Video content)

Validations will take effect when the record is published. If the record is not valid, publication fails, and editors need to fix the content to ensure all rules are handled before proceeding to move the record into the Published stage.

---

# Legacy batch operations endpoints will be sunset on March 10th

Source [product-updates]: https://www.datocms.com/product-updates/deprecated-batch-operations-endpoints-will-stop-working.md

[date: 2025-01-07T09:46:08.600+01:00]

Starting March 10th, 2025, our legacy batch operation endpoints — which have been deprecated for years — will be discontinued and will return error responses. Documentation for these endpoints can be found here:

-   [https://www.datocms.com/docs/content-management-api/resources/item/batch\_destroy](https://www.datocms.com/docs/content-management-api/resources/item/batch_destroy.md)
-   [https://www.datocms.com/docs/content-management-api/resources/item/batch\_publish](https://www.datocms.com/docs/content-management-api/resources/item/batch_publish.md)
    
-   [https://www.datocms.com/docs/content-management-api/resources/item/batch\_unpublish](https://www.datocms.com/docs/content-management-api/resources/item/batch_unpublish.md)
    

### Required action

If you're using any of these endpoints, please update your integration to use their replacements. Documentation for the current endpoints can be found here:

-   [https://www.datocms.com/docs/content-management-api/resources/item/bulk\_destroy](https://www.datocms.com/docs/content-management-api/resources/item/bulk_destroy.md)
-   [https://www.datocms.com/docs/content-management-api/resources/item/bulk\_publish](https://www.datocms.com/docs/content-management-api/resources/item/bulk_publish.md)
    
-   [https://www.datocms.com/docs/content-management-api/resources/item/bulk\_unpublish](https://www.datocms.com/docs/content-management-api/resources/item/bulk_unpublish.md)
    

### Impact assessment

Based on our monitoring over the past two months, only four projects are actively using these deprecated endpoints. If your project is affected, you will receive a direct email from our team in the coming days.

---

# Enhancements to Structured Text

Source [product-updates]: https://www.datocms.com/product-updates/enhancements-to-structured-text.md

[date: 2024-10-31T10:15:41.853+01:00]

We've recently pushed out a relatively large UX improvement to working with Structured Text fields in DatoCMS.

Aside from slash commands and markdown, editors can also manage the formatting of their content with the new upper toolbar, bringing in a more familiar experience.

(Video content)

While the floating toolbar on selected content still exists, focusing on a Structured Text field now reveals a new top toolbar with the following options:

-   Text formatting for setting text as headers, paragraphs, and quotes,
-   Regular formatting options for bold, italic, strikethrough, underlined, and highlighted content,
    
-   Inserting toolbar icons to trigger custom plugins,
-   Inserting code blocks,
    
-   Generating bullet and numbered lists,
-   Inserting links and dividers, and
    
-   Embedding blocks and models if the Structured Text field has validations in place for it.
    

(Video content)

The new toolbar is also applied to embedded blocks that have a Structured Text field within them.

(Video content)

Note: The new toolbar will only show on the active Structured Text editor. For example, if you have a Structured Text block embedded within the editor, you will see the toolbar within that block until you re-interact with the parent field.

Get up to speed with all the capabilities of the field [on the docs](https://www.datocms.com/docs/content-modelling/structured-text.md).

---

# New Plugin Hooks

Source [product-updates]: https://www.datocms.com/product-updates/new-plugin-hooks.md

[date: 2024-10-31T10:14:25.257+01:00]

We’ve made some considerably large updates to Plugins in DatoCMS.

It’s now possible to insert and interact with plugins in 3 new locations within the CMS, making content operations much more flexible for your editors and use-cases.

### **Dropdown Actions**

We've introduced hooks to implement custom dropdown actions across various parts of the CMS:

**Record-Editing actions**

(Image content)

Give editors custom options when interacting with specific record types for use-cases like triggering workflows or interacting with their content in specific ways.

The hooks required for their implementation are:

-   Present the actions using [`itemFormDropdownActions()`](https://www.datocms.com/docs/plugin-sdk/dropdown-actions.md#itemFormDropdownActions)
-   Execute the action with [`executeItemFormDropdownAction()`](https://www.datocms.com/docs/plugin-sdk/dropdown-actions.md#executeItemFormDropdownAction)
    

**Field-Specific Record Actions**

(Image content)

Create custom options when interacting with specific fields for advanced use-cases like translations or interaction with other content proofing tools.

The hooks required for their implementation are:

-   Present the actions using [`fieldDropdownActions()`](https://www.datocms.com/docs/plugin-sdk/dropdown-actions.md#fieldDropdownActions)
-   Execute the action with [`executeFieldDropdownAction()`](https://www.datocms.com/docs/plugin-sdk/dropdown-actions.md#executeFieldDropdownAction)
    

**Global Record Actions**

(Image content)

And finally, give more control to editors when interacting with Records in bulk, to unlock use-cases like mass-editing or enhancing Records without having to enter each one individually.

The hooks required for their implementation are:

-   Present the actions using [`itemsDropdownActions()`](https://www.datocms.com/docs/plugin-sdk/dropdown-actions.md#itemsDropdownActions)
-   Execute the action with [`executeItemsDropdownAction()`](https://www.datocms.com/docs/plugin-sdk/dropdown-actions.md#executeItemsDropdownAction)
    

### **Asset Sidebars and Sidebar Panels**

Another long-requested feature, similar to the sidebars on Content Records, you can now create plugins for the asset views.

(Image content)

You can create individual panels within the existing sidebar view, or

(Image content)

Create entire sidebars altogether.

This is particularly nifty for use-cases like adding custom data/fields into asset properties, or enriching the CMS to offer editors a view on how their assets would look across specific devices, for example.

The implementation is similar to how you'd handle creating plugins for the Content Record sidebars, with the addition of asset-view specific hooks:

-   [`upload​Sidebars`](https://www.datocms.com/docs/plugin-sdk/sidebar-panels.md#uploadSidebars) and [`render​Upload​Sidebar`](https://www.datocms.com/docs/plugin-sdk/sidebar-panels.md#renderUploadSidebar) for entire sidebars, and
-   [`upload​Sidebar​Panels`](https://www.datocms.com/docs/plugin-sdk/sidebar-panels.md#uploadSidebarPanels) and [`render​Upload​SidebarPanel`](https://www.datocms.com/docs/plugin-sdk/sidebar-panels.md#renderUploadSidebarPanel) for individual sidebar panels.
    

### **Record Collection Outlets**

Record collection outlets allow you to add custom areas to the views where you see content records of a certain model.

(Image content)

You can add plugins to the tabular, compact, and tree views for models, and can use it for a variety of use-cases like content workflows, instructions, and other general information that is relevant to an entire model.

The implementation is similar to that of Record Form outlets, with specific hooks added in:

-   `itemCollectionOutlets;` to declare the intention to offer Record Collection Outlets, and
-   `renderItemCollectionOutlet` to display the Outlets.

---

# New Plugin Installation Experience

Source [product-updates]: https://www.datocms.com/product-updates/new-plugin-experience.md

[date: 2024-12-17T11:03:35.990+01:00]

We’ve redesigned the Plugin installation experience to make discovering and installing plugins from the marketplace faster and more intuitive—directly within your DatoCMS project.

### **How It Works**

To add a plugin, go to **Configuration \> Plugins** and click on **Add a new plugin**. This opens the Marketplace, where you’ll find all community plugins displayed as detailed cards, giving you essential information at a glance.

Clicking on a plugin reveals its detailed view, including its description, metadata, and additional information. From there, simply click the **Install** button to start using your plugin immediately.

### What's hot and what's not

We’ve introduced brand new collections to make it even easier for you to pick and choose relevant plugins for your project!

(Video content)

**Editor Favorites** to showcase plugins that enhance the content and editorial workflows with commonly used plugins loved by editorial teams.

**Dev Favorites** to highlight the most commonly installed plugins by project owners to make daily operations and new features easier to manage.

**Enterprise and Workflows** to curate common plugins for security, compliance, and custom third party integrations used by teams of larger sizes.

---

# Consistency improvements to sidebars in DatoCMS

Source [product-updates]: https://www.datocms.com/product-updates/consistency-improvements-to-sidebars-in-datocms.md

[date: 2024-12-10T09:05:14.728+01:00]

We weren't too happy about the inconsistencies and the discoverability of the expand/collapse actions with sidebar interactions in the CMS, so we've released some improvements.

The sidebar component retains it's open/closed states, and we've redesigned the component to have consistent interactions with instant transitions.

(Video content)

This change is reflected on all sidebars to the left and right of screens on content views, asset views, plugins, and within the project settings.

---

# Bulk Actions for Modular Content

Source [product-updates]: https://www.datocms.com/product-updates/bulk-actions-for-modular-content.md

[date: 2024-10-15T11:31:11.808+02:00]

Managing Modular Content is now faster and more efficient with the addition of **Bulk Actions**. You can now easily select multiple Modular Content items and perform actions all at once from a brand new action bar.

### **Key Features**:

**Bulk Operations**

(Video content)

Each Modular Content row now includes a checkbox for easy selection and you can perform bulk actions such as:

-   Select All / Invert Selection
-   Expand / Collapse selected blocks
    
-   Copy multiple blocks
-   Delete selected items
    

**Block Options**

(Video content)

We've also revamped the contextual submenu to make managing blocks a bit easier, with an improved flow to:

-   Copy & Paste
-   Duplicate
    
-   Move
-   Delete, and
    
-   Add Blocks

---

# Introducing the new DatoCMS Remote MCP

Source [blog]: https://www.datocms.com/blog/introducing-the-new-datocms-remote-mcp.md

Posted on [date: 2026-05-26T12:15:51.099+02:00] by Ronak Ganatra

Now, as a marketer myself, I can definitely tell you that I don't use AI to create my content and I'm keeping it natural. Insert wink wink meme. If you're the same as me, then you've probably been doing a lot of copy-pasting from the content you've been manually creating in ChatGPT or Claude or wherever else. Prompt something in Claude, copy it into DatoCMS, edit it. Ask ChatGPT to translate something, paste it back in. Do this for every new record but split the copy paste operations into as many fields as that model has. I mean. It gets the job done, but it's a bit silly when you think about it. I hate how much my `cmd`, `c`, and `v` keys are wearing out.

The new DatoCMS MCP server fixes that. It's a secure connection between your AI assistant of choice and your DatoCMS project, so instead of pingponging content back and forth between tabs, you just describe what you need and the AI handles it directly.

(Video content)

Never heard of an MCP before? Don't worry, we'll quickly look into that.

Heard of an MCP before? Check out our [user guide on using the DatoCMS MCP for content](https://www.datocms.com/user-guides/content-management/working-with-the-datocms-mcp.md).

And if you're a dev who's going to primarily work with the CLI, the MCP is still really cool and all, but you should totally [check out working with DatoCMS Skills](https://www.datocms.com/blog/the-new-datocms-agent-skills.md) for that lil extra something something.

## What's an MCP

We're not looking to be the SEO authority on What is an MCP, so if you're keen to dive in to details, check out the [Model Context Protocol](https://modelcontextprotocol.io/docs/learn/server-concepts) docs. It's basically a standard way for AI assistants to connect to external tools and actually do things in/with/through them, and not just talk *about* them.

Without an MCP, your AI assistant is like a very smart colleague who can only tell you what to do in X tool. With an MCP, it's like giving them a comfy little chair and having them do it for you.

Once it's connected, you can ask your AI assistant to do things in DatoCMS in plain language, and it will do that on your behalf (note: on your behalf means AS YOU. Your account is what will be used by your AI).

## What you can actually do with it

About anything you'd normally do by clicking/typing around in DatoCMS, you can now just ask your AI assistant to do instead:

-   "Create a new blog post with this title and body content and ensure all validations are met."
-   "Add German and Italian translations to all landing pages published in April."
    
-   "Unpublish all the published records, I'm not happy with them."
-   "Find all records that are missing an SEO description and list them for me. And for all the records with an SEO description, audit them and improve them for me to review before publishing."
    
-   "Update the author field on these 15 records to link to the new author profile."
-   "Under Product Categories create a new category for socks, and then create 50 dummy products with unique images before localizing them into Mandarin and trigger the translation workflow for me."
    
-   "Ooh I forgot to add images. Install the Unsplash Asset Source plugin for me, and add 10 images of shawarmas for me to look at and connect to the posts."
    

It handles long records, lots of fields, deeply nested content structures, localizations, constraints, etc. The previous version of our MCP ([yeah, we had one](https://www.datocms.com/blog/we-have-released-an-mcp-sometimes-it-works.md)) could struggle with complex content. This one doesn't.

## You're in control of what it can touch

When you connect your AI assistant to DatoCMS via the MCP, you can choose exactly which projects it can access, and it only ever has the same permissions your DatoCMS account has. It can't access anything you can't access yourself.

On top of that, read operations (things like looking things up, searching, listing records) can run freely without any extra confirmation. Write operations, which is anything that creates, updates, or deletes content, will require your explicit approval before they run.

So the assistant will tell you what it's about to do and wait for your OK. Nothing happens behind your back.

## How to connect it

The whole point of making this remote is that there's nothing to install. No terminal, no dev environment, no asking your developer to set something up.

(Video content)

For Claude (web or desktop):

Go to Customize, then Connectors, click the plus button, select Add custom connector, enter `DatoCMS` as the name and `https://mcp.datocms.com` as the URL, and click Add. Then hit Connect and log in with your DatoCMS account.

For ChatGPT:

Enable Developer Mode under Settings, then create a new app with the MCP Server URL set to `https://mcp.datocms.com`. Connect and authenticate.

Both work in the browser and on mobile. [Full setup instructions for every supported client are in the docs](https://www.datocms.com/docs/mcp-server.md).

## One script, not fifty tool calls

Most MCPs work by exposing API endpoints directly. When you ask your fave LLM to do something, it makes a tool call, gets a result, makes another tool call, gets another result, and on and on. For anything big, this gets expensive fast (in tokens and in round-trips and in time) and the agent tends to lose context and coherence across a long chain of individual calls.

Our MCP does things a little bit differently. When you give it a task, it writes a TypeScript script that batches the entire operation (multiple API calls, conditional logic, whatever is required) and executes it in an isolated runner through one execution, not fifty shades of tool calls.

The practical result is that operations that would have been slow or unreliable before, like long records, deeply nested blocks, batch localizations, or bulk updates across many records, now complete fast and accurately.

## Read vs. write: two tools kept separate

The MCP has two execution tools and they behave differently by design.

**Read-only operations** use `upsert_and_execute_safe_script` which run against a read-only API token and most clients will let them through without a confirmation prompt.

**Write operations (and anything destructive)** uses `upsert_and_execute_unsafe_script` which has full read-write permissions, and the client asks for your explicit confirmation before running. So a setup where reads are always allowed and writes require approval is just the default, with nothing to configure on your end.

## Beta note

This is a beta. It works well, but there are usage limits depending on your DatoCMS plan, and we may adjust things as we learn more from real-world usage. If something doesn't behave as expected, we genuinely want to hear about it.

PS: If you're a developer, [check out the new Agent Skills](https://www.datocms.com/blog/the-new-datocms-agent-skills.md) we're launching alongside this.

---

# When to use Skills vs. the DatoCMS MCP

Source [blog]: https://www.datocms.com/blog/when-to-use-skills-vs-the-datocms-mcp.md

Posted on [date: 2026-08-19T11:37:21.457+02:00] by Ronak Ganatra

Oh look, a 100% human written article about using AI (I'd love to say its because I'm idealistic, but in reality, hello [Text Watermarking](https://www.anthropic.com/news/claude-text-watermark) in the EU 🤭).

Anyways.

[Our own docs are pretty opinionated about when to choose Skills and/or the MCP](https://www.datocms.com/docs/ai-overview/overview-of-ai-and-automation.md). Working in a local repo? [Install Agent Skills](https://www.datocms.com/docs/agent-skills.md) and skip the MCP. No Terminal? Skip skills and [use the MCP](https://www.datocms.com/docs/mcp-server.md).

Pick one.

Not both.

Much binary.

But that's ✨mostly✨ correct, considering best practices, for about 90% of what you'll do.

It doesn't account for my own laziness, and since I reach out for the MCP inside Claude Code all the time, let me contradict things and introduce a third option.

## Understanding Agent Skills and the MCP

[Agent Skills](https://github.com/datocms/agent-skills) are markdown playbooks for your agent to load on demand. They wrap the CMS CLI, they know how to model content, they know how to wire up a frontend, they know how to write a clean migration, they know how to build a plugin, all the fun deeply technical stuff. They run with the full permissions of your machine, so they can read, write, run shell, hit the network, etc., etc. They're built for... well, building.

The MCP is a slightly more guarded approach to managing your content. The difference, is what Claude would call **LOAD BEARING**. With OAuth and no tokens on disk, the MCP uses a layered approach with tools rather than 500 raw endpoints, giving you an efficient way to manage content operations. Every script it runs is sandboxed, read-only by default, and never actually sees your API tokens. It's built for when there's no terminal.

But realistically speaking, can the MCP "build" your project? Yeah, of course. [I cover it in the user guide myself](https://www.datocms.com/user-guides/content-management/working-with-the-datocms-mcp.md). But the reason we suggest one rather than the other isn't capability, it's applicability. Skills reach *into* your machine, the MCP *holds back* on what the agent can touch.

## The reason we split the use

The docs say "use the MCP to help your content and editor teams improve their workflows; use Skills when you're working against DatoCMS API".

What this means is: Terminal open? Use Skills. No Terminal? Use MCP.

(Image content)

If you're a dev with the IDE open, you want the agent to do the thing, correctly, on the first try, instead of confidently hallucinating field types that don't exist.

Skills come in two packs, the project pack, for building a project, and the plugin pack, for building plugins. Here's a quick TLDR of the capabilities:

-   **Content Modelling**: The agent can figure out whether to use models or blocks, references or embedded blocks, taxonomies, field shapes, validators, editor appearances, all the stuff that needs brainwork.
-   **Reading Content**: The agent can do things like "write a GraphQL query to fetch all blog posts with images", and infer things like filters, pagination, localisation, modular content, Structured Text, responsive images, SEO, etc. etc.
    
-   **Writing Content and Automation**: The agent can run programmatic CMA scripts, record CRUD, do bulk importa/exports, manage assets, fork environments, handle webhooks, manage roles and tokens, check audit logs, and, OK I'm running out of adjectives.
-   **CLI Workflows**: The agent can "properly" handle schema-type generation, run environment ops, CI/CD pipelines, other CMS imports, etc.
    
-   **Frontend Integrations**: The agent can manage web previews, visual editing, cache tags, sitemap wiring, and so much more, across Next.js, Nuxt, SvelteKit, and Astro.
-   **Plugins**: Create, extend, and design plugins anywhere in the CMS.
    

All of this is code related. It lives ON your machine, it's version-controlled, and it wants senior dev instincts baked in just like what you expect when you start your prompt with "take on the role of a principal/staff web developer who used to be a lead frontend designer but also is a systems architect as a side hustle". It's why we recommend using Skills to do everything you would with your Terminal/IDE, because Skills already wrap the DatoCMS CLI.

The MCP on the other hand, is designed to make life easier when there's no Terminal involved, which is why, although it CAN manage schema stuff, we recommend it for editors. Or for developers who want to be a little lazy and do things on the fly from their browser or mobile.

The MCP is great for when you need to handle:

-   **Everyday content ops**: creating records with several fields, updating records, publishing records, unpublishing records.
-   **Translations**: Adding translations to existing content, one at a time or in bulk.
    
-   **Assets**: Uploading assets and attaching them to records without much fuss.
-   **Linking and Copying**: Linking records together or copying content between models.
    
-   **Querying**: Listing and querying records to find the thing you're after (literally, I've used it for "find me the link to the record where I mentioned Clippy" because I couldn't find it.
-   **The boring stuff**: Bulk updates, content migrations, SEO updates, all the stuff that's a chore.
    

The MCP reaches your project through OAuth to do things *AS YOU*; nothing to install, nothing to configure, no tokens, nothing.

So while one CAN do the others' job, it's not HOW we designed it to, because we think one wins over the other depending on the use case.

## The cheeky third option

But what if you're lazy AND technical? I mean, Claude Code is both, a terminal AND a fluent MCP client. Why choose one. WHY HAVE US TELL YOU WHAT TO DO. You weren't going to listen to us anyways. You don't want to choose. To you I say:

(Image content)

***Full disclosure****: This is what I do ALL the time. I just use the MCP in Claude Code to handle everything in one prompt. I'm not saying I'm good at it, I'm just lazy, and I'm terrible at prompting. This is not recommended best practice, I am not responsible for anything breaking on your projects, don't @ me!*

(Image content)

OBVIOUSLY I meant commit to Git, but I wasn't going to go rewrite it and take a new screenshot when this whole post is about pro-lazy workflows...

So nothing's stopping you running both in one session. The question was never can you. It's when's it actually the smarter move.

Here's a few scenarios in which I combine the MCP with Skills to make my life a little easier (and boss-man's code reviews a little more annoying).

### Anything across more than one project

Skills wrap your local repo's CLI. One repo. One project.

The MCP OAuths into everything you have access to: personal account, orgs, the lot, and lets you operate on any of them in the same session without restarting anything.

So when I need to modify the schema in an environment on one project, but bring in assets from another project, or reference the SEO from a third project to create into the first project, I combine skills with MCP capabilities in the same session to feel like I'm actually useful.

### Bulk-YOLO-ing on PROD

Say I need to re-slug 3000 posts, rewrite the internal links, add redirects, and republish. On production.

Claude with Skills would happily do it with the full permissions of my machine and my CLI token, which is exactly as relaxing and therapeutic as it sounds when the target is live content.

Through the MCP, things are different. The agent explores read-only first (the safe script variant physically blocks every non-GET request at the network layer, and it can't write even if the model gets ideas). Then, and only then, it asks me to confirm the write. It never sees my real token. Every API method it calls has to be pre-declared, or the script gets rejected before it runs.

That's slower. So I string things together. MCP the re-slugging first, then bulk publish, then instantly Skills add the old slugs to new slugs on redirect and push, deploy. Lovely handoff.

### Bigger refactors

This is my favourite, because it uses both tools for exactly what they're good at, and it's quite likely to be an approach you'll encounter, unlike the previous two where I'm clearly doing things I shouldn't

Before I write a real migration, I check the live data through the MCP. How many records actually use this field? Which locales are half-baked? Read-only, no risk, no repo acrobatics. No bearing of any loads.

Then I take what I learned and write prompt the permanent, version-controlled migration in the CLI thanks to Skills.

In those military terms that Claude loves so much: MCP as the recon drone. Skills as the demolition crew.

---

So in summary, when should you use the MCP vs. Skills?

| Use case | Where you probably are | Reach for |
| --- | --- | --- |
| Schema, frontend, migrations, plugins | Repo/IDE | Skills |
| Content ops, any kind | Web/Mobile | MCP |
| Content ops on live / foreign / multiple projects | IDE | MCP in the terminal |
| Building AND heavy live content ops, same session | IDE | Both |

What you do need to consider though (honest caveat, because I have the same limits as you, I didn't get some all-access-no-limit version of the MCP):

-   [**The MCP is token hungry**](https://www.datocms.com/docs/mcp-server.md#limits): We've scoped it REALLY well to minimise consumption by taking a tool-based approach, but it's still got an appetite. It pulls full method docs and examples for accuracy (unless you give it the very specific bits, but who remembers that), which is great for success rates and rough on your token count. For pure build work, Skills are leaner. Don't run the MCP for a job Skills already own.
-   [**Limits gonna limit**](https://www.datocms.com/docs/mcp-server.md#limits): Per-script timeouts, monthly time budgets, etc. Massive undertakings will hit these limits, for genuinely big projects and migrations, batch them into chunks and use the CLI.
    
-   **Don't run both for the same thing**: Even when combining, use each approach for different aspects of the same thing, don't blitz your token with asking one to redo or recheck the other's work all the time.
    

SO, all that's left is to then get started 👇

**Skills**:

Terminal window

```bash
/plugin marketplace add datocms/agent-skills
/plugin install datocms@datocms-skills
```

**MCP**:

Terminal window

```bash
claude mcp add --transport http DatoCMS https://mcp.datocms.com
```

✌️

---

# Cache Tags for eCommerce: Surgical invalidation, and then some

Source [blog]: https://www.datocms.com/blog/cache-tags-for-ecommerce.md

Posted on [date: 2025-07-03T08:54:18.955+02:00] by Ronak Ganatra

### TL;DR

[Cache tags in DatoCMS](https://www.datocms.com/blog/introducing-datocms-cache-tags.md) let you selectively and surgically invalidate content, instead of blowing up your entire CDN cache on every content update. For eCommerce projects on headless sites, that means faster performance, fresher content, lower infrastructure costs, and a much better experience for both developers and users. Whether you're using DatoCMS for product content, or have a PIM in place and keeping the CMS just for landing pages and blogs, lets check out how cache tags come in to play.

**Quick resources to get started:**

-   [**Docs on Cache Tags**](https://www.datocms.com/docs/content-delivery-api/cache-tags.md)
-   [**Everything you need to know about the feature**](https://www.datocms.com/blog/introducing-datocms-cache-tags.md)
    
-   [**Using Cache Tags with Next.js**](https://www.datocms.com/docs/next-js/using-cache-tags.md)
-   [**Check out the Cache Tags starter to get familiar with using them**](https://github.com/datocms/nextjs-with-cache-tags-starter)
    

OK. Let's get into it.

### **The Performance v. Freshness Problem**

eCommerce sites are dynamic by nature. Shoes go out of stock. Skateboards go on sale. BFCM rolls into town and the discounts rain down.

Non-promotional content changes constantly as well: product descriptions, CRM banners, price callouts, category intros, blog posts, AB Tests... we could go on. Most of this doesn’t justify a global cache purge or a full rebuild, but that’s still how many teams handle updates. As a result, users either get stale content or slower sites. Or worse. False information.

And aside from all that, it's just really expensive.

You can’t afford either. Performance erodes conversion. Staleness erodes trust. Rebuilds and cache purges erode your wallet.

The solution is surgical: only invalidate what actually changed. That’s why we build cache tags after all, to solve that exact problem for us.

### What exactly are Cache Tags

Stefano's gone into a [great in-depth dive around everything](https://www.datocms.com/blog/introducing-datocms-cache-tags.md) this feature is and does, so check that out! In the meanwhile, let's over-simplify it in the context of an eCom shop.

Cache tags are lightweight string identifiers automatically included in the HTTP response headers when you query content from the CDA. They map to specific records in your project, be it a banner, a post, a product, whatever.

Query a product? You might get something like:

```http
HTTP/1.1 200 OK
...
X-Cache-Tags: XXXX XXXX XXXX XXXX XXXX XXXX

{
  "data": {
    "product": {
      "title": "Pink Lace-up Boots",
      "inStock": "Y",
      "_createdAt": "2025-06-01T15:19:24+01:00"
    }
  }
}
```

When any of those records update (via publishing or API), you can call the Admin API to purge juuuust those tags rather than the cache of the project or that content type:

Terminal window

```bash
curl -X POST https://site-api.datocms.com/cache/purge \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "tags": ["XXXX XXXX XXXX XXXX XXXX XXXX", "YYYY YYYY..."] }'
```

That invalidates only the record/content that depends on those records. If you’re caching entire page responses or relying on ISR/SSG, that can dramatically reduce build and invalidation overheads.

Pretty nifty eh?

### When your CMS is your PIM

Some eCom sites use DatoCMS as the main content layer for products, collections, promos, and campaigns. This is a straightforward solution for many storefronts with small-to-medium sized catlogs who don't need a full-blown PIM to handle their inventory.

In this setup, cache tags let you invalidate cache based on what changed across your storefront. For example:

-   A single product update invalidates the cache for any corresponding product page(s) and any categories it's part of.
-   A promo ends and its banner is unpublished. Only the affected homepage sections are invalidated and rebuilt.
    
-   A category description is updated. That collection page is rebuilt or revalidated.
    

This saves infrastructure costs, because you’re not regenerating or purging unaffected pages. It also improves time-to-freshness. Most updates can reflect within seconds (or faster) without needing a full redeploy.

It also makes it possible to ship changes faster. Editors can publish without triggering full rebuild workflows or waiting for engineering. Devs don’t need to write cache logic from scratch or debug stale content reports.

### When your PIM is your PIM

OK but what if you're a much bigger brand and you've got 1000s upon 1000s of products managed from the PIM. That's the source of truth right? So why care about the cache on CMS content as much?

This is the more common setup for most bigger shops as it is, where all the critical product data lives in a PIM. DatoCMS handles content like banners, campaign landing pages, blog posts, category descriptions, SEO content, and mayyybe product visuals if that's not in a PIM or standalone DAM.

Cache tags still matter here.

Even if your PDPs pull stock and pricing from your PIM, they may still render dynamic modules (badges, cross-sells, promo blocks) from DatoCMS. When that content changes, cache tags give you a direct way to invalidate only the relevant cache.

Let's visualize that.

Say your homepage has six dynamic sections like a hero, promo banner, featured products, trust badges, brand story, and latest blog teaser, each powered by a different DatoCMS record. An editor updates just the Black Friday hero block.

Without cache tags, your frontend might purge and rebuild the entire homepage, invalidating CDN responses across all regions. That’s slow, inefficient, and wasteful. But with cache tags, you can invalidate only the specific record, this `BFCM_banner` that changed. The rest of the homepage stays cached, fast, and untouched.

This keeps loads on your CDN and build infrastructure minimal. There are no traffic spikes from unnecessary re-renders, no stale content, and no surprises. Content updates stay scoped to exactly what changed.

### Cache Tags and DX

For developers, cache tags bring predictability and control.

First, they’re transparently exposed in every response. You can inspect them in your logs or edge functions and map cache tags to pages or components, they're not some easter egg you'll stumble upon or struggle to find. That gives you a clear dependency graph for purge operations every time.

Second, they integrate well with most frameworks. In Next.js, for instance, you can build ISR fallback logic that listens to Dato webhook payloads and revalidates paths based on updated tag IDs.

Here's how [Cache Tags play along with Next.js](https://www.datocms.com/docs/next-js/using-cache-tags.md).

And [with Astro](https://www.datocms.com/blog/astro-typescript-graphql-and-datocms-cache-tags.md). Bonus is: That's a real-life usage of Astro and Cache Tags for our own website.

Third, they work with external caches. If you’re caching GraphQL responses at the edge, you can store cache tags as metadata and selectively expire keys when tags are purged. It's not one over the other, they play nicely with your existing setup.

### But what about the content team?

Well, the editorial team mostly won't (and actually shouldn't) be interacting with cache tags DIRECTLY, but they have also simplified how we handle preview content workflows. In setups where editors expect to preview content before publishing, cache tags allow the frontend to bypass stale caches without introducing complex logic or rebuild pipelines.

Because tags reflect specific record dependencies, preview environments stay snappy and accurate without special exceptions.

This creates a much smoother editor experience. Content teams can confidently publish or tweak components knowing that previews are fresh and production reflects changes really quickly. From a dev perspective, it removes an entire class of hard-to-debug issues around preview staleness and lets you treat preview mode with the same tag-based invalidation logic you use in production.

### OK but let's talk $$$

Yeah that all sounds cool, but LET'S TALK BUSINESS BENEFITS.

Well. Every cache miss costs time and money. Every full rebuild increases infra cost. Revalidating 300 pages because one changed is wasteful on all fronts.

Cache tags give you a way to avoid that by making content invalidation proportional to actual change.

This might be hard to see for small projects, but matters more at scale. If you’re a retailer with hundreds of landing pages or localized content in multiple markets, the cost difference adds up fast. With cache tags, a change to a single DE promo page doesn’t need to touch your EN, FR, or ES versions. Your builds stay fast and scoped.

And freshness improves. Instead of waiting 5-10 minutes for a deploy pipeline to complete, you can push updated content live in seconds. Your editors don't have to wait to see their changes live, they can basically continue with their workflows uninterrupted.

And for the end user?

A much better UX. Your customers get accurate content, timely offers, and no weird inconsistencies.

---

Cache tags aren’t just a nice-to-have, they’re a critical tool for keeping content-driven eCommerce sites fast, fresh, and cost-effective. If you’re using DatoCMS in any capacity, wire in cache tag-based invalidation. It takes an afternoon to implement and saves you countless hours in edge cases, rebuilds, and support.

---

# Introducing DatoCMS Agent Skills

Source [blog]: https://www.datocms.com/blog/the-new-datocms-agent-skills.md

Posted on [date: 2026-05-26T12:14:40.889+02:00] by Ronak Ganatra

**Quick Links:**

-   [Documentation and installation instructions](https://www.datocms.com/docs/agent-skills.md) to get up and running
-   [Open source on GitHub](https://github.com/datocms/agent-skills) so you can inspect every skill, contribute, or fork
    

---

If you've ever asked Claude to help you build something with DatoCMS and watched it confidently hallucinate completely wrong GraphQL queries, invent API methods that never existed, or just generally YOLO something into production, then this is for you.

The problem isn't the model. It's context. Your agent doesn't always know how DatoCMS works, what our conventions are, or how our pieces fit together. It knows enough to sound confident, which is arguably worse than knowing nothing.

## The setup you probably already have

Story time. You've got VS Code open with a terminal running and Claude or Cursor is somewhere doing their thing. You're building something with Dato, and you want your LLM to actually do something useful rather than confidently make sh\*t up.

(Image content)

That setup is exactly what Skills are built for. And it pairs naturally with the [DatoCMS CLI, which just landed a HUGE batch of changes](https://www.datocms.com/blog/major-changes-to-managing-content-programmatically.md) that make the two feel like they were designed to be together because in so many ways, they were.

In a nutshell.

The CLI can now run TypeScript scripts directly against your project via `datocms cma:script`. In stdin mode, there are no imports, no boilerplate, just top-level `await` with `client` and `Schema` available as globals, and full typechecking before anything touches the API. This mode was explicitly built for agentic workflows: your agent writes the script, pipes it to the CLI, and the type system catches wrong payload shapes before they execute.

`datocms schema:inspect` lets your agent examine models, fields, and relationships without opening the UI, and pass a filter or omit it to walk the whole project. `datocms cma:docs` now shows TypeScript signatures alongside the docs in the terminal, with flags to drill into type definitions without leaving your editor context.

But we're not here for that, we're here for skills. Skills know all of this. When your agent is working on a migration, querying structured text, or scaffolding a plugin, it's not guessing at the right patterns, it has the exact everything it needs for exactly this environment.

## Introducing datocms/agent-skills

Skills are markdown-based playbooks that your agent loads on demand. Each one covers a specific area of DatoCMS work with the kind of depth that lets your agent get things right on the first attempt: the right API shapes, the right patterns, the right conventions. You don't invoke them manually — describe a task in plain language, the agent matches it to the right skill automatically, and it has everything it needs to get to work. No prompt magic required on your end.

If you don't have a local repo open — say you're working from the web or mobile, or you're an editor managing content — check out [the brand new MCP server](https://www-draft.datocms.com/blog/introducing-the-new-datocms-remote-mcp.md) we just launched. It's great for discrete, targeted tasks and needs zero local setup. We actually recommend it especially for your editors and content team.

Agent Skills are for developers in longer coding sessions, where the agent needs more than just API access — it needs to understand how DatoCMS *realllly* works, what the conventions are, how things fit together. Without that guidance, you end up with an agent that can call the API but doesn't really know what it's doing. Skills fix that. And every MCP capability is already baked in via local CLI calls, so if you're in a repo, Skills are all you need.

Here's a quick overview of what they can do:

-   **Content modeling** — schema-design decisions: model vs block, references vs embedded blocks, taxonomies, field shapes, validators, editor appearances.
-   **Reading content** — GraphQL queries against the Content Delivery API: filters, pagination, localization, modular content, Structured Text, responsive images, SEO metadata, typed queries with gql.tada or codegen.
    
-   **Writing content & automation** — programmatic CMA scripts: record CRUD, bulk imports/exports, asset uploads, environment forks and promotions, webhooks, roles and tokens, scheduled publishing, audit logs.
-   **CLI workflows** — migrations, schema-type generation, typed CMA scripts, environment operations, CI/CD pipelines, WordPress/Contentful imports.
    
-   **Frontend integrations** — draft mode, Web Previews, Visual Editing, Content Link overlays, real-time preview subscriptions, cache-tag invalidation, SEO/sitemap wiring across Next.js App Router, Nuxt, SvelteKit, Astro, plus `react-datocms`, `vue-datocms`, `@datocms/svelte`, and `@datocms/astro`.
-   **One-shot setup** — bootstraps multi-step flows like "set up draft mode and visual editing" or "wire up migrations" in a single command, queueing prerequisites automatically.
    
-   **Plugin development** — create a brand-new plugin from scratch with the Vite/React structure, picking the initial surfaces (field extensions, config screens, sidebars, pages, asset sources).
    

Since skills hand off tasks to each other like besties, the default is that all skills will be installed together.

## Wait. You're just going to mention MCP as a BTW?

Fair question, and worth answering properly. [The full details are in the dedicated post](https://www.datocms.com/blog/introducing-the-new-datocms-remote-mcp.md).

The short version: MCP and Skills aren't competing — they cover different people and different kinds of work.

**MCP** works from any interface — browser, mobile, chat apps — with zero local setup. It's great for discrete content operations: small schema tweaks, adding translations, updating a batch of records, anything where you know what you want and just need the agent to execute it. Your content team can use it. Your PM can use it. Your cat can use it. You can use it too, for quick things.

**Skills** are for developers with a repo open. Longer coding sessions where the agent needs more than API access — it needs to understand how DatoCMS actually works, what the conventions are, and how the pieces fit together within your codebase. The CLI changes above are a good illustration: `cma:script`, `schema:inspect`, `cma:docs` are powerful primitives, but an agent without context will use them wrong. Skills give it that context. And every MCP capability is already built in via local CLI calls, so there's nothing to miss.

If you're shipping code, install Skills and skip the MCP server.

## Getting Started

For **Claude Code**, install via the marketplace for auto-updates and namespaced invocation:

Terminal window

```bash
/plugin marketplace add datocms/agent-skills
/plugin install datocms@datocms-skills
```

Same for **Codex**:

Terminal window

```bash
codex plugin marketplace add datocms/agent-skills
```

For **all the others,** use the universal npx installer:

Terminal window

```bash
npx skills add datocms/agent-skills --skill '*'
```

[Check out the docs and installation guide](https://www.datocms.com/docs/agent-skills.md) to get started with your tooling preference.

The whole thing is open source at [github.com/datocms/agent-skills](https://github.com/datocms/agent-skills). Worth checking out really quickly before granting an agent access to a sensitive repo.

## Please help us improve!

Both, Skills, and the new MCP, are in beta. If something breaks or behaves unexpectedly, the feedback links in the docs are the right place to shout. And we really can't stress this enough - we'd LOVE your feedback when using Skills and the MCP - the good, the bad, and especially the ugly.

---

# Records, finally typed: full TS support in the DatoCMS JS client

Source [blog]: https://www.datocms.com/blog/records-finally-typed.md

Posted on [date: 2025-10-10T11:45:54.880+02:00] by Stefano Verna and Ronak Ganatra

### TL;DR

-   The [DatoCMS JavaScript client](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients.md) is now 100% type-safe! Records were the only part not completely typed before — we’ve taken care of that.
-   The [CLI now perfectly bridges your schema and repo](https://www.datocms.com/docs/cli.md), generating complete TypeScript definitions mapped to your project automatically.
    
-   The client also introduces new utilities to simplify complex record management. Functions like `duplicateBlockRecord()`, `inspectItem()`, or `mapBlocksInNonLocalizedFieldValue()` are now ready to use and handle nested blocks at any level.
-   All record-related docs have been rewritten with new TypeScript examples showcasing the updated helpers and workflows.
    

### Records, finally typed end-to-end!

The missing piece is finally here. Records are no longer the odd ones out — they’re now fully typed, inferred, and validated by your compiler just like everything else. Say goodbye to `unknown`s right where clear types were needed the most. The new JS client is TypeScript from the ground up, with a [CLI](https://www.datocms.com/docs/cli.md) that [generates types directly from your project’s schema](https://www.datocms.com/docs/content-management-api/resources/item.md#generating-types-from-your-schema).

(Video content)

No docs were opened in the making of this demo.

Now, your code “sees” your real model structure. Every field you define in your schema is mirrored as a TypeScript type for some very specific and juicy autocomplete magic. And if your schema changes, your compiler will be the first to know.

### A short view back to the past

Before this release, the client was already rock-solid for the [Content Management API](https://www.datocms.com/docs/content-management-api.md) — migrations, schema updates, all well-typed and reliable — but **records** were the weak spot. Because every model in the CMS is unique, there wasn’t a single “record shape” the compiler could enforce. Fetching and updating records often meant handwritten definitions or falling back to `any`, so schema changes could slip through until runtime. That’s the gap we’ve closed.

Now the client is fully type-safe, records included. Every method, every field, every filter — with inference, autocomplete, and compiler checks baked in.

If you’re deep into TypeScript, this finally feels… right.

### Your schema, but then make it TypeScript

This is where the CLI comes in, acting as a bridge between your repo and schema.

Terminal window

```bash
$ npx datocms schema:generate schema.ts
```

It introspects your project, grabs your schema, and [generates TypeScript definitions](https://www.datocms.com/docs/content-management-api/resources/item.md#generating-types-from-your-schema) for all your models and fields.

(Video content)

Change something in your schema? Run it again. Types update instantly. Aaaand the compiler immediately highlights stale code.

Here’s how it plays out:

-   📝 **Rename a field?** Switch `description` to `summary` in your Blog Post model, and suddenly every `post.description` in your code lights up in red like a Christmas tree. Fix it once, move on.
-   ❌ **Remove a field?** Your compiler calls you out before you even hit save. Not in production, not at runtime — *right there* in your editor.
    
-   ⚡ **Tweak your schema?** Run the command again. Boom — fresh types, all synced up.
    

No extra steps. No hand-maintained files. Just a one-liner that keeps your schema and your code perfectly in tune.

### But wait. There's more!®

The client now also ships with [dozens of brand-new utilities](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#block-processing-utilities) that make complex record operations far simpler for things that used to take hours to deal with properly.

Functions like [`duplicateBlockRecord()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#duplicateblockrecord), [`inspectItem()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#inspectitem), or [`mapBlocksInNonLocalizedFieldValue()`](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#mapblocksinnonlocalizedfieldvalue) are now ready to use. They manage nested blocks, relations, and localized fields automatically. We're also introducing the new [`SchemaRepository`](https://www.datocms.com/docs/content-management-api/using-the-nodejs-clients.md#schemarepository-utility-for-efficient-schema-access) class to have an efficient caching layer ready in your code, perfect for scenarios where you need to repeatedly access the same schema information.

[Take them for a spin!](https://github.com/datocms/js-rest-api-clients/tree/main/packages/cma-client#block-processing-utilities)

### If you love gql.tada, you’ll feel right at home 🪄

If you’ve ever used tools like **gql.tada**, **GraphQL Code Generator**, or **urql codegen**, you know that warm, fuzzy feeling of fully typed queries and autocompleted fields. That’s exactly the kind of developer experience you get here — but for our REST Content Management API.

Your schema drives your code. Filters, `order_by`, Structured Text, blocks and Modular Content — they’re all fully typed and checked against your actual models.

-   Typo in a field name? Won’t compile.
-   Trying to order by a field that doesn’t exist? Won’t compile.
    
-   Querying a key you deleted last Tuesday? Yeah, that won’t compile either.
    

And type-safety isn’t just for reading data. **Creates, updates, and block operations get the same treatment** — even in migrations. Just regenerate your types after running them, and you’re good.

No more silent mismatches. No more schema drift. No more runtime surprises.

### Docs got an overhaul too!

These are big changes. The docs need to reflect all of it, of course, which is why **they've been completely reworked for records**. All record-related pages now show examples that combine full typing with the new helpers, so you can see exactly how to read, transform, and update content, with explanations that match how you’ll actually write code today.

Records, filtering, ordering, and Structured Text with blocks are shown exactly as you’ll use them in a more IRL context, with generated types in the imports and field names that match reality.

Dive in to the docs to check out the new examples and start working with the CLI and the new JS Client and let us know how things are for you!

-   [Type-safe Development with TypeScript](https://www.datocms.com/docs/content-management-api/resources/item.md#type-safe-development-with-typescript)
-   [Create a new record](https://www.datocms.com/docs/content-management-api/resources/item/create.md) (9 new examples)
    
-   [Update an existing record](https://www.datocms.com/docs/content-management-api/resources/item/update.md) (11 new examples)
    

---

So there we have it. With the new updates, your schema and your code are finally in sync. Your compiler won’t let you ship broken assumptions. And your editor will autocomplete the fields you need without any docs detours. Everything in its right place. Like it was meant to be.

To **take full advantage** of all this, make sure you update to the **latest versions** of both the JavaScript client and the CLI. Here’s how to install them (assuming you use DatoCMS):

Terminal window

```bash
$ npm install @datocms/cma-client-node @datocms/cma-client-browser @datocms/cli --save-dev
```

Or globally (for the CLI):

Terminal window

```bash
$ npm install -g @datocms/cli
```

Once upgraded, you’ll be ready to generate types straight from your schema, benefit from strict record typings, and keep your workflows safe and predictable.

---

# Major CLI & CMA changes for managing content programmatically

Source [blog]: https://www.datocms.com/blog/major-changes-to-managing-content-programmatically.md

Posted on [date: 2026-05-18T15:42:04.442+02:00] by Ronak Ganatra

### TLDR

The last couple of weeks brought a set of converging improvements across the CLI, the JS CMA client, and the structured-text packages.

-   `datocms cma:script`. Run ad-hoc TypeScript against your project from the CLI, no boilerplate required, standard input mode built for agentic/LLM workflows
-   `datocms schema:inspect`. Inspect models, fields, and relationships without opening the UI
    
-   `schema:generate` now emits `Schema.X.ID` and `Schema.X.REF` runtime constants alongside types
-   New `FieldValueInRequest<T, K>` helper family for typing CMA write payloads correctly
    
-   `isBlockWithItemOfType` predicate for narrowing structured text block nodes to a specific model
-   New `datocms-structured-text-dastdown` package to serialize and edit DAST documents as plain text
    
-   `mapNodes` upgraded to support full structural tree rewrites (splat, remove, transform)
-   [Records](https://www.datocms.com/docs/content-management-api/resources/item.md) and [Update a record](https://www.datocms.com/docs/content-management-api/resources/item/update.md) docs fully rewritten with end-to-end examples
    
-   To update: `npm i -g @datocms/cli` for CLI changes, `npm i @datocms/cma-client-node@latest` for client changes, `npm i datocms-structured-text-dastdown` for the new package.
    

---

The not-so-great truth about working with the CMA programmatically for a while was that the API itself is powerful, but getting it to behave well with TypeScript (or inside an automated pipeline) required more manual work than it should have.

So we made some dramatic changes to the CLI for the CMA.

This batch of changes addresses both:

-   Better TypeScript inference across the board so the type system catches mistakes before they hit the API.
-   And a ✨new✨ set of CLI primitives that make the CMA usable from automated workflows and LLM-generated scripts.
    

## Closing those annoying TypeScript gaps

Working with block fields through the CMA always required you to know a loooot of context up front: what shape the API accepts on writes (different from what it returns on reads), how to type an accumulator when rebuilding a block array, how to narrow a cluster of blocks down to a specific model... The client didn't help thaaat much either. There were times when fields came back as `unknown`, and wrong payloads only surfaced as `422`s at runtime.

The new `FieldValueInRequest<T, K>` helper family fixes the write-side typing gap. You pass it the record type and a field key, and it gives you back the exact TypeScript type the API expects for that field on a `create` or `update` call, including what each block entry needs to look like.

```typescript
const page = await client.items.find<Schema.LandingPage>(id, { nested: true });

// typed accumulator
const sections: NonNullable<FieldValueInRequest<typeof page, 'sections'>> = [];

for (const block of page.sections) {
  if (isBlockOfType(Schema.HeroBlock.ID, block)) {
    sections.push(
      buildBlockRecord<Schema.HeroBlock>({
        id: block.id,
        headline: block.attributes.headline.toUpperCase(),
      })
    );
  } else {
    sections.push(block.id); // keep unchanged blocks as IDs
  }
}

await client.items.update<Schema.LandingPage>(page, { sections });
```

Three variants cover different points in the data flow.

-   `FieldValue<T, K>` for standard responses,
-   `FieldValueInNestedResponse<T, K>` for when you've fetched with `nested: true`, and
    
-   `FieldValueInRequest<T, K>` for what you're sending back. `T` accepts a fetched record, a narrowed block, or a `Schema.X` marker directly.
    

For structured text specifically, there's now `isBlockWithItemOfType`/`isInlineBlockWithItemOfType` which is a predicate that narrows a block node inside a DAST tree to a specific model shape in one step. No extra casting, and it works both, as an inline guard, and as a curried predicate for `findFirstNode` / `filter` / `find`:

```typescript
// inline guard
if (isBlockWithItemOfType(Schema.CtaBlock.ID, node)) {
  // node.item.attributes is now typed as CtaBlock
}

// curried predicate
const firstCta = findFirstNode(content, isBlockWithItemOfType(Schema.CtaBlock.ID));
```

Oh. And `schema:generate` now emits runtime constants alongside types (`Schema.Article.ID` and `Schema.Article.REF`) so you can stop hardcoding item-type ID strings:

```typescript
// value position now generated from your project
await client.items.create({
  item_type: Schema.Article.REF,
  title: 'Hello world',
});

if (item.relationships.item_type.data.id === Schema.Article.ID) { ... }
```

## The CLI can now run scripts directly

Getting a CMA script running previously meant bootstrapping a proper TypeScript project with the whole imports, client setup, tsconfig, etc. etc.. which is a bearable overhead for migrations you're saving, but it's a choooore for one-off jobs.

`datocms cma:script` fills the gap between `cma:call` (single API operations, limited) and spinning up a full external TypeScript project just to run a script. You get a pre-authenticated client from `datocms link`, `--api-token`, or an environment variable, and two modes depending on what you need.

**stdin mode** is the fast path , no exports, no imports, just top-level `await` with `client` and `Schema` available as globals:

Terminal window

```bash
npx datocms cma:script <<'EOF'
await client.items.create<Schema.Article>({
  item_type: Schema.Article.REF,
  title: 'Hello world',
});
EOF
```

Full typechecking runs before anything hits the API. This is also the mode designed for agentic workflows so an LLM can write the script and pipe it to the CLI in a single step, with the type system catching wrong payload shapes before they're executed.

**File mode** is for anything that needs local helpers or editor LSP support:

Terminal window

```bash
npx datocms cma:script tmp/scripts/backfill-slugs.ts
```

The file uses the migration function signature (`export default async function(client: Client)`), so when a one-off script is worth keeping, it moves into `migrations/` without modifications.

**`schema:generate`** **now emits runtime constants.** `Schema.Article.ID` and `Schema.Article.REF` are now generated from your project alongside the TypeScript types, so you can stop hardcoding item-type ID strings that drift out of sync across envs:

```typescript
// type position: the model's TS shape, as before
const article = await client.items.find<Schema.Article>(id);

// value position: the model's id and ref, generated from your project
await client.items.create({
  item_type: Schema.Article.REF,
  // …
});

if (item.relationships.item_type.data.id === Schema.Article.ID) {
  // …
}
```

**`datocms schema:inspect`** lets you [inspect DatoCMS models and modular blocks](https://github.com/datocms/cli/tree/main/packages/cli#datocms-schemainspect-filter) to emit their structure, fields, and relationships. Pass an exact or fuzzy filter (API key, ID, or display name) to narrow the scope, or omit it to list the entire project.

The command defaults to a compact [TOON output](https://github.com/toon-format/toon) and basic field data. Use the `--json` flag to format output for pipeline tools like `jq`, and flags like `--include-validators` or `--fields-details=complete` to expand field verbosity.

You can also walk the schema graph using specific inclusion flags:

-   `--include-nested-blocks` — recursively includes nested blocks.
-   `--include-referenced-models` — pulls in models referenced by link or structured text fields.
    
-   `--include-embedding-models` — fetches models that embed the target blocks.
    

**`datocms cma:docs`** now shows the TypeScript signature of the matching client method alongside the docs, so you can see argument shapes and return types without leaving the terminal. Two new flags (`--expand-types` (inline specific or all reachable type declarations) and `--types-depth` (control how deep the type walker descends)) let you drill into the type definitions when you need more detail.

## Editing Structured Text without touching the AST

Structured Text content lives as a DAST tree which a nested structure of typed nodes. Any programmatic edit has required either writing traversal logic against the AST directly, or skipping it. Neither was really that practical for common operations like bulk find-and-replace across articles, brand renames, or feeding structured content to an LLM for a rewrite.

The new `datocms-structured-text-dastdown` package gives you a different option. Serialize the DAST tree to a readable markdown-like format, edit it as plain text, and parse it back:

```typescript
import { parse, serialize } from 'datocms-structured-text-dastdown';

const cur = await client.items.find<Schema.Article>('article-id', { nested: true });

// 1. serialize to dastdown
const text = serialize(cur.body);
// 2. edit as plain text
const edited = text.replace(/Acme Corp/g, '**Acme Inc.**');
// 3. parse back, reusing the original document so untouched blocks
//    keep their original payload by reference
const body = parse(edited, cur.body);

await client.items.update<Schema.Article>('article-id', { body });
```

Embedded blocks appear in the serialized output as `<block id="..."/>` placeholders so you can move or delete them, but their internal fields are opaque at this layer. For editing block contents or doing structural tree rewrites, you use `mapNodes` from `datocms-structured-text-utils` which now supports full structural transforms, not just 1:1 node mapping.

Return a single node, an array to splat into siblings, or `null` to remove.

The LLM angle here is the same as with `cma:script`: dastdown output is plain text a model can process directly and pipe back to `parse()`. Bulk content rewrites, tone adjustments, translation, or any text operation that would previously have required understanding the DAST format can now be handed off to a model with a straight string in and string out.

This really is a great fit for text-heavy content where edits are textual and may cross node boundaries. Think articles, docs, and chapters. Its' not really recommended for landing pages made up of opaque blocks.

## Two new tools for working with your schema

`datocms schema:inspect` is a new CLI command that lets you examine models and blocks without opening the UI. Pass a filter (API key, ID, and display name with fuzzy matching) or omit it to just list the whole project:

Terminal window

```bash


# compact overview
npx datocms schema:inspect article

# full details for pipeline use
npx datocms schema:inspect article --json --fields-details=complete | jq '.fields'

# walk the graph
npx datocms schema:inspect landing-page --include-nested-blocks --include-referenced-models
```

And `datocms cma:docs` has been enhanced so it now shows the TypeScript signature of the matching client method alongside the documentation, and two new flags (`--expand-types` and `--types-depth`) let you drill into type definitions without leaving the terminal.

## The records docs got a full rewrite

To cover this in a lot more detail, the [Records guide](https://www.datocms.com/docs/content-management-api/resources/item.md) and [Update a record guide](https://www.datocms.com/docs/content-management-api/resources/item/update.md) have been completely rewritten. They now cover block field operations (modular content, single block, structured text) with actual before/after terminal outputs for every operation, and a full treatment of localization update rules, bulk block operations across the content hierarchy, and optimistic locking. Check them out.

---

# "It's just CRUD", and other famous last words when thinking 'I can just build a CMS with vibes'

Source [blog]: https://www.datocms.com/blog/build-a-cms-with-vibes.md

Posted on [date: 2026-04-27T14:30:32.041+02:00] by Ronak Ganatra

So. [You read the last post](https://www.datocms.com/blog/who-needs-a-headless-cms-when-you-can-just-vibe-code-everything.md)? You nodded along? And then you got to the end and thought "ok but I could just build a CMS, it's literally just CRUD, how hard can it be"?

Valid. Very valid.

Lots of people do that.

(Image content)

This is basically a genre at this point. Every other Tuesday someone posts that they've replaced their CMS with a weekend project, or want to build one in X framework and markdown files and good vibes.

There is barely ever a follow-up post later.

(Image content)

What's changed recently though, is that they're now not wrong about the easy part being easy per se. Like actually right. For example, [Leerob migrated cursor.com off a CMS over a weekend with $260 in tokens and 344 agent runs and wrote about it](https://leerob.com/agents) honestly and I'm not going to pretend that's not impressive because it genuinely is. In this case, the dev, the team, the use case, the stack, the stars, and the chakras, and then some, all seemed to have aligned.

The "I just built a CMS" is now a whole thing. But for the rest of us, it isn't as straightforward. So if you're not him, the rest of this is for you.

**Technically. Vibe building a CMS** ***IS*** **just CRUD. For like, a weekend.**

No really.

Need a simple blog? Have a `title`, `body`, `slug`, a `publish` button, a basic admin panel put together... That's what? An afternoon with a good prompt tops. The core of what a CMS does is not complicated - you have content, you need a place to add it, and a way to publish it to a site that has a way to ask for it.

And honestly? If your use case stops here and your whole team are developers, with nobody non-technical ever touching the content on a site has a few page types and a blog that updates every now and then, then go for it.

Build the thing. Keep it in the repo. Markdown your content. Vibe away. No gaslighting you into thinking you need a SaaS subscription for that use case.

BUT.

Yeah, there's always a but. Let's dive into that but.

The mistake isn't building a "CMS". The mistake is not knowing where "CMS" actually ends.

### **What you're actually still missing**

So the CRUD work is done and it works. You show it to someone and they say "this is great, can I schedule posts?"

Huh. Oh right. That's Cron jobs, right?

[Scheduled publishing](https://www.datocms.com/docs/general-concepts/scheduled-publishing-unpublishing.md). Sounds like a cron job. Kind of is. Claude, do your thing.

Oh but wait. There's also stages for what needs to be scheduled? That content needs to be a draft until the Cron publishes it, and what happens when the scheduled post fails silently at 2am, and whether that triggers a rebuild, and what the editor sees when they check in the morning and the post isn't live yet and they ping you on Slack.

Huh. I guess its time to build Build Triggers. That sounds like an easy [webhooks](https://www.datocms.com/docs/general-concepts/webhooks.md) project. Simple prompt.

Then Marta from [marketing wants a preview](https://www.datocms.com/features/editor-experience.md) before publishing or scheduling. We're not even talking about "[Visual Editing](https://www.datocms.com/features/visual-editing.md)" as a requirement.

Not a "open localhost:4321" preview.

A "WTH is an IDE and why should I need one to see what I've written? Why do I need to install node and learn all about npm scripts for that?" preview.

A "I need to send this URL to bossman who is on their phone and does not have a GitHub account and definitely does not know what a branch is" preview.

Draft mode, shareable URLs, no login. Ok, this is starting to get a liiiiitle annoying.

Then comes another "can you restore the homepage from last Tuesday" request. Uhhh. [Revision history](https://www.datocms.com/docs/general-concepts/versioning.md). Rollback. Who made the change and when. Sh\*t.

Then a blog post needs to reference an author. The author has a bio page. Someone updates the author's name. It should update everywhere automatically. Now you're building a relational content layer.

When did you sign up for this?

You see where this is going.

The issue with "I'll vibe a CMS" mostly boils down to misunderstanding the tooling and maintenance needed, which nobody seems to be putting into the estimate.

This is where the "it's basically free" math meets the "I'm great at prompting" approach, and things quietly start to fall apart.

### But wait, there's more**™**

Now, another myth in the CMS building world is that content is *just* simple text and structure so the database approach is just fine, because everything online is a table or a form anyways, and needs to find a way to live in a database. The reality is that a CMS treats the C as more than that. It is a database yes, but it's reallllly opinionated on *what* constitutes as "content".

Assets, localizations, and other abstractions of "data" are what a CMS is really optimized for. So let's address those inclusions for the vibe build.

[**Media**](https://www.datocms.com/features/images-api.md)**.** Uploads, CDN delivery, image resizing, focal points so the crop doesn't cut someone's head off, alt text, video encoding. You can vibe code a basic asset uploader in a few prompts (Leerob did exactly this and it works fine for a team where everyone's a developer). It's also not the same thing as a media pipeline that survives a team of editors uploading a 47MB PNG and wondering why the site is slow. DAMs are hella specialized in managing all that overhead and headache, so unless you also want to build a DAM, a CMS solution is definitely needed.

[**i18n**](https://www.datocms.com/features/headless-cms-multi-language.md)**.** Not just a locale field where you can maintain a JSON of translated strings, but translation states: which fields are translated, which aren't, which are in review, which need updates, which localizations are outdated compared to the main locale, locale fallbacks so you don't serve a blank page when the German version isn't done yet, a way to have different editors manage German content for Germany v. for Austria, locale-specific publishing... the list goes on. Oh, and of course, the moment where your AI-at-build-time translation tool doesn't play nicely with your content model and you need to either build a plugin or hire someone. Localization is such a monster, even with choosing a CMS it isn't always a one-size-fits-all, much less so when building a CMS for a smaller use case.

**Webhooks and build triggers.** We already touched upon this, but workflows aren't *just* create and publish. There's cache invalidation, triggering deploys on publish, triggering unpublishes, managing different frontends with different build parameters, what happens when an incorrect record breaks the build at midnight and nobody's watching the logs because field validations weren't considered in the beginning?

[**SEO fields**](https://www.datocms.com/user-guides/content-modeling/intro-to-the-seo-fields.md)**.** Meta titles, descriptions, OG images, per-locale overrides, canonical URLs, structured data. You'll add these when someone asks why the page isn't showing up right on Google, and non-technical people don't want to address another batch of JSON strings when creating content, nor do you want to reconfigure your UI to add more fields and manage things retroactively because SEO overrides and fallbacks weren't part of the original scope.

My point is, none of these are impossible to build individually. All of them together is a CMS.

Which is the "complex" thing you were trying not to build, but are in fact, "basic expectations" when choosing a CMS.

But these are just features, there's a whole other mammoth under the surface.

[**Security and governance**](https://www.datocms.com/features/data-integrity.md)**.**

Two editors is genuinely all it takes before you have a permissions and access problem.

Who can publish? Who can only draft? Who can translate what? Who changed something accidentally? Where is the data stored? Can things be backed up? Are there environments? Who broke the schema? Who mislabeled a field in the repo? What happens when the new intern deletes the homepage? How do we handle GDPR content deletion requests? Can I slap on SSO because the IT department said so without rebuilding Auth0 or Clerk? And this is just the tip of the iceberg. I could ask hundreds of hypotheticals around this topic.

Your vibed-up database has no opinion about who should be allowed to nuke your pricing page on a Friday afternoon.

You'll need to build that opinion in. Then document it. Then explain it to the next person who inherits the codebase after you leave, which I know you're not planning to do because I barely ever document my grocery shopping list properly.

Going back to the earlier reference of Leerob's (no shade, I legit think what he did is mad impressive), this is the exact reason why even our friends at [Sanity shared their take on why not to build a CMS](https://www.sanity.io/blog/you-should-never-build-a-cms) and this bit here summarizes all my thoughts too:

(Image content)

### Oh. And about that "free" thing

The math everyone does when approaching to vibe a CMS is monthly CMS subscription cost vs. zero (and by zero I mean LLM subscription excluded because we have one for 50 other things).

The math everyone skips is the hours to build scheduled publishing, the afternoon the media pipeline broke, the contractor brought in because the i18n plugin wasn't good enough, the week the new hire spent understanding a system nobody documented because the person who built it thought it was obvious, the two days of chaos when that person left, and the time and effort needed to build ad-hoc feature inclusions like a DAM or Auth or SSO or whatever else.

Vibe coding brought the build cost way down. Like super down. But it didn't change the maintenance cost. It didn't change the ownership cost. And it sure as hell didn't change the "we need this new thing and you built this so it's your problem figure it out" cost, which is the most annoying one.

The monthly subscription you were avoiding wasn't *just* paying for a database and a shiny little UI.

It was paying for ten years of other people hitting these exact walls so you don't have to.

### **So when does vibing a CMS actually make sense?**

I said it at the top but I'll be more specific: [Leerob's situation was real and the call totally made sense](https://leerob.com/agents). All developers. One website. Mostly prose. Stable scope. Dev tooling where even non-devs are hella technical. A CDN bill that had gotten expensive for specific and fixable reasons (read: hosting video through the CMS CDN might not have been ideal for a site with Cursor's traffic).

> We’re saving thousands of dollars in CDN usage by moving to lower cost object storage. As a side effect, build times are 2x faster by cutting out network I/O going to the CMS when prerendering pages.

If your whole team lives in the terminal, nobody non-technical will ever touch the content, and your scope is small and stable and unlikely to grow, yeah, go ahead. Build it. Keep it simple. Even if you build a UI for non-technical users and you really think the overhead is manageable because its' not a massive team, it could still make sense.

The question is whether that's actually your situation or just your situation right now.

The moment someone who doesn't know what a pull request is needs to update something independently? You're not building a CMS anymore, you're running one, which is a very different scope.

---

# Introducing Tuply: The AI Context Assistant for the next Headless CMS Era of Tomorrow

Source [blog]: https://www.datocms.com/blog/introducing-tuply.md

Posted on [date: 2026-04-01T08:55:16.889+02:00] by Stefano Verna , Ronak Ganatra , Roger Tuan , Matteo Papadopoulos , Matteo Giaccone , Matteo Balocco , Marcelo Finamor Vieira and Irene Oppo

The way we create content is changing.

Teams are moving faster than ever. Stakeholder expectations are higher. The pressure to deliver personalized, performant, omnichannel digital experiences, while maintaining brand consistency across a growing content graph, has never been greater.

At DatoCMS, we've been listening. To our customers. To our partners. To the broader ecosystem of developers, content strategists, and digital experience leaders who trust us to be at the center of their content infrastructure. Essentially, listening to you.

And what we've heard, consistently, is this: the tools need to work harder so you don't have to.

Today, we're proud to announce the most significant product investment DatoCMS Marcelo has made in years ever.

Meet **Tuply**.

(Video content)

## A New Kind of AI for a New Kind of Content Team

Tuply isn't just another AI feature. It's a rethinking of what intelligent assistance means inside a composable content architecture.

We didn't want to bolt AI onto existing workflows and call it a day. We wanted to ask a harder question: *what does it look like when AI is truly ambient? When it understands context, not just commands?*

The answer, after months of research, iteration, and collaboration with some of our most forward-thinking enterprise customers, is Tuply: a persistent, context-aware AI presence that lives directly inside your DatoCMS environment, learning how your team works, anticipating your needs, and showing up exactly when it matters most.

This is AI that meets you where you are. Not a modal. Not a sidebar. Not a separate tab. Tuply is woven into the fabric of your editing experience, quietly intelligent, always available, ready to add value the moment you need it.

*Much like Clippy, who was ahead of his time (pour one out 🥃)*

(Image content)

## Built on a Foundation of Real Customer Insight

Nothing about Tuply was built in a vacuum.

Over the past several months, we ran an extensive closed beta with content teams across industries, e-commerce, media, enterprise SaaS, digital agencies, gathering qualitative and quantitative signals about where AI could meaningfully reduce friction in the content creation lifecycle.

We looked at where editors were losing time. Where context-switching was killing flow. Where the gap between *intent* and *output* was widest. We brought in researchers. We ran workshops. We synthesized findings across hundreds of hours of interviews.

And across every segment, every team size, every content maturity model, one theme emerged with striking consistency: *Sometimes people just needed a little pick-me-up.*

(Image content)

Tuply uses a retrieval system over a curated corpus of jokes, selected by hand, which were assessed for quality using human judgment, which is a form of intelligence, which is technically natural intelligence rather than artificial intelligence but we feel the line is blurring and we'd prefer not to revisit this question.

Yes. The answer is no.

Tuply is a separate initiative.

Tuply does not appear on the roadmap.

Tuply simply *is*.

---

Ok, as if it wasn't obvious, happy April 1st from all of us. No, we're not sorry. Tuply is a small cartoon character that lives in the corner of your screen and tells jokes. That's it. That's the whole thing. It'll be gone tomorrow.

We shipped it yesterday evening. It took Marcelo about four hours, including the time he spent making the eyes blink. Every year we feel like doing a lil something something for April Fools' Day and every year we run out of time. This year we said f\* it, let's do something, so now we're bullying marketing into writing a blog post about it at 11pm.

We don't fully understand how we got here either.

---

# Who needs a Headless CMS when you got vibes?

Source [blog]: https://www.datocms.com/blog/who-needs-a-headless-cms-when-you-can-just-vibe-code-everything.md

Posted on [date: 2026-03-27T12:03:09.847+01:00] by Ronak Ganatra

### TLDR

-   Vibe coding is pretty dope. A solo dev can spin up a fully functional site or an app in an hour. I'm not here pretending that's not true.
-   A CMS and vibe coding solve different problems. One accelerates *how* you build and one manages how your content *lives* after you've shipped.
    
-   If you're the only person touching the content, and you're a developer, you might not need a CMS. If anyone else does, or if your content needs to outlive your current stack, then you probably do.
-   AI slop generated content is a volume play, not a content strategy. Filling your site with just slop generated content faster doesn't solve the content problem.
    

Every few months a new discussion starts up for a hot "do you even need X/Y/Z anymore" take. We've seen "do you need a backend," "do you need this framework," and now with the rise of Cursor, Bolt, Lovable, and whatever tool dropped last Tuesday, we're firmly in "[do you even need a CMS](https://www.reddit.com/r/vibecoding/comments/1psz2kt/cms_is_dead_long_live_ai_coded_websites/)" territory.

(Image content)

Fair question, tbh.

If you can describe a website in plain English and have a working app scaffolded in 20 minutes, the case for adding yet another tool to the stack gets harder to make. I mean, I've vibe coded a bunch of stuff, and not just side projects to land up in the dreaded side project graveyard a couple of weeks later, so I'd rather approach this honestly than gaslight you with "101 reasons why you absolutely need a Headless CMS in the AI era we're seeing in 2026, 2027, and beyond", which is just another low effort SEO title that won't rank because that'd be ridiculous.

Anyways. CMS vs just vibes.

### What a CMS *actually* does

Let's start here because the word "CMS" has been stretched into near meaninglessness by years of vendor positioning between website builder, CMS, app builder, CaaS, hosted backend, DXP, and heavens know what else. Let's not get into the whole "empower your editorial team" or "future-proof your content operations" or "superduper charge your customer's digital experience" fluff, let's just say what it does.

[A Headless CMS separates your content from your code](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md). That's all. That's what it's always done. That's the foundational thing. It means the person updating the homepage banner doesn't need to open a pull request. It means a typo in a product description doesn't require a deploy. It gives non-developers a place to work with content without touching the codebase.

Beyond that, a [CMS enforces a schema](https://www.datocms.com/features/schema-builder.md) that makes your content predictable and structured. If you've defined that a blog post has a title, a slug, a cover image, a body, and an author, every blog post has those things. You're not hunting through a JSON that's gotten messier over two years because three different people added fields differently.

It doesn't HAVE to, but more often than not, a [CMS also handles media stuff](https://www.datocms.com/docs/general-concepts/media-area.md): uploads, CDN delivery, image optimizations, alt text... It also manages who can publish what and when and how and where. It gives you [audit trails](https://www.datocms.com/docs/general-concepts/audit-logs.md). It [handles localizations](https://www.datocms.com/docs/general-concepts/localization.md) to translation states, locale fallbacks, schedules, versions, metadata, y'know, the stuff that's incredibly boring to build yourself and also incredibly necessary the moment you hear the dreaded "OK, but will this scale?"

I'm not going to pretend that any of this is glamorous. A CMS is infrastructure. The whole value prop is that it's just boring, stable, and invisible. When it's working right.

### Now. What vibe coding gives you

Real talk. Quite a lot.

In fact, I'm going to let Claude handle this just to see how he/she/it summarizes my thoughts after reading what I've written till this point on.

<AI-slop\>

*The modern AI-assisted dev workflow is remarkable for solo developers and small teams. Cursor can scaffold a data model and wire up a basic CRUD UI faster than you could write the boilerplate by hand. Bolt can take a screenshot of a design and get you 80% of the way to a working component. For prototypes, internal tools, hackathon projects, and MVPs — this stuff is legitimately game-changing.*

*The issue is that vibe coding is great at building the shape of a thing. It's less great at thinking about how that thing lives, grows, and gets maintained over time.*

*When you vibe code a site, your content is typically living in one of a few places: hardcoded in components, in flat files (Markdown, MDX, JSON), or in a database your app owns. None of these are inherently bad. But none of them come with a schema enforcer, an editorial interface, a media pipeline, or a way for your marketing manager to update a page without pinging you on Slack at 4pm on a Friday.*

*Cursor doesn't know your editorial calendar. It doesn't know that Lena in marketing needs to push a campaign banner live tomorrow and doesn't have VS Code installed. The AI generates the structure just fine. The problem is everything that happens to that content after the code ships.*

</AI-slop\>

Honestly, not terrible. I mean its' not how I type (I'm pretty sure I'm allergic to the word *gamechanger* to begin with), but it gets the point across pretty well. You just know that content was AI generated even if I wouldn't have mentioned it.

It—smells—like—AI.

Which brings us to...

### The AI Slop problem

Because a non-trivial number of devs vibe coding things are also asking if they can just vibe the content out too. Yeah, you can.

You can generate 500 articles in an afternoon. You can publish a fully automated website that spits out like 4 posts a day till the end of time. You can fill your site with copy about every topic even remotely related to your product/service/offering and have every page, FAQ, landing page, whatever, written by an LLM in time for an early and long lunch. But we'd all know it.

(Image content)

From the same discussion linked at the beginning

The content is cheap to produce and cheap to read, all you need is some good prompts and a little `CLAUDE.md` or some instructions somewhere for the LLM to know your tone/industry/vibe/etc. BUT. It often lacks a specific point of view that comes from expertise and experience, it doesn't reflect your actual experience with a problem, and its just really easily recognizable by both readers and search engines now. "Comprehensive guide to X" posts that are really just a topic outline with confident filler aren't new, but yet, here we are, surrounded by it.

Also, just generating *more* content doesn't fix a content strategy that wasn't working nor is it a content strategy by itself. If your blog wasn't driving traffic before, publishing 50,000 posts won't change that.

I'm not saying don't use AI at all (I do, that'd be super hypocritical of me). I use Claud-*ia* 💁‍♀️ for early drafting, some translations, and generating variants for testing, or just generating easy TLDRs, but its' more of a workflow thing. A CMS on the other hand manages the dreaded logistics and the output of real editorial work. If the content isn't worth managing in the first place, no tooling decision is going to save that.

**SO. We need humans? Right?**

Depends on how many people you got, what your site/app/product is, and how content-dependent it is on growth.

If the answer is zero where you're a solo dev, you're the only one writing and publishing, or if you're building small side projects that're going to top-out at a few pages/posts, keep cracking on with your `.md`s on a flat-file setup. I'm not going to tell you otherwise.

There's also the middle ground - where your website is a blend of static pages and posts, like our own. It doesn't need to be a "CMS or AI" question, both can coexist (I mean there's a reason so many CMS are betting on [AI features and MCPs](https://datocms.com/features/ai) and trying out agentic workflows).

(Video content)

Like on [our own website](https://github.com/datocms/astro-website) - you can do a mix of hardcoded or not, if there are places where editors are not involved and it's just easier to handle by code (see the navigation/footer or complex sections of the site). All our posts, guides, articles, docs, and text-heavy content gets handled in the CMS, but for most static-ey pages, I myself vibe code changes allll the time (*just don't tell bossman*).

But things change when you got a team. Not even a large team. Just others (especially non-technical ones) who need to make changes to pages and posts. The moment they need to work independently from you, and you don't need/want to give them access to the repo, or have varied flavors of roles and permissions, you have a content management problem.

And what solves content management problems? Content management systems.

### When you need a CMS

Is this where I start shameless 🔌 territory? Absolutely.

(Image content)

**Multi-locale websites and storefronts and products.** The moment you and/or your team is managing content in more than one language, you have to think about things like translation states, fallbacks, and locale-specific publishing and un-publishing. Building this yourself *issss* doable. Maintaining it across contributors is a whole other story. Got external translators coming in or need idiomatic nuances between `de-DE` and `de-AT`? Yeah, that's a CMS thing, and most Headless CMS are built to handle [modern websites](https://www.datocms.com/use-cases/modern-websites.md) and [eCommerce sites](https://www.datocms.com/use-cases/ecommerce.md).

(Image content)

**Marketing sites with any kind of editorial cadence.** If someone is publishing blog posts, campaign pages, or product announcements more than occasionally, they need an interface. Not a code editor. And you really don't want them to ping you every time somethings' needed for you to go and edit JSON files and open PRs to just make simple pushes.

(Image content)

**Editorial/News sites.** Ok this should be an obvious one, because, well, journalists and editorial teams collect tons of information to write content - and I've seen tons of "tiny" news sites spewing unverified news to have a sitemap of 1000s of pages which you can tell are slop. So *can* you run a news/editorial site on vibes hooked up to some News API? Yeah. But wow are they (mostly) terrible. The moment content operations is core to your business strategy, getting a [Headless CMS for editors](https://www.datocms.com/use-cases/digital-publishing.md) to do their thing becomes really important.

(Image content)

**Anything intranet-ey or knowledge base-y.** Support portals, docs, forums, and things like this need to be constantly updated, changed, created, and usually have several people contributing to them. Vibing that for anything beyond a simple website with 5-10 "knowledge" pages is a nightmare waiting to happen. Getting a [Headless CMS that's optimized for knowledge management](https://www.datocms.com/use-cases/knowledge-management.md) makes a ton of sense.

**Anything multi-channel.** Same content delivered to a web app, a mobile app, and maybe an email or a digital display? A Headless CMS is the cleanest answer to "how do we avoid managing five separate copies of this."

[**Content workflows**](https://www.datocms.com/features/workflow-cms.md)**.** Not every team can have anyone push content to production. There's regulated industries, enterprises, multi-brand setups... these need staging, reviews, proof-checks, and role-based access to ensure permissions are honored. Again, that's CMS territory.

**Content that needs to outlive the stack.** This one's reallllly underrated because I've rewritten my own site with content SO many times from WP to Ghost to Gatsby to Next to Astro, going from WP posts to Ghost posts to markdown. Every time I've moved it I've had to painfully redo so much (and pour one out for when I decided I neeeeed TypeScript for my 5 posts with less than 2 monthly visitors). Finally I gave up and moved all my stuff to a CMS so I just need to modify the template/query stuff, not the actual content. If you hardcode content into a Next.js app and then rebuild in Astro two years later, migrating that content is a choooooore. If your content lives in a structured CMS with a clean API, the frontend is just a view layer to requery things.

**Just anything with more than a few people, really.** The moment you have more people contributing to the growth of a website or app, you'll need a CMS. There's only so much you can accomplish with vibes before wanting to rip your hair out AND babysit everyone's demands.

### When you can skip the CMS and go all-in on vibes

I won't try to convince you that you neeeed a CMS for things like these.

(Image content)

**Personal projects and portfolios.** If it's your own site, you write all the content, and you're a developer, just use MDX, or hardcode it, or whatever you're comfortable with. A CMS is overhead you really don't need.

**Purely functional apps with UGC.** If your "content" is user-generated data stored in your own database like reviews, form submissions, app data, events, things like that, then that's not really necessarily a CMS use case. That's a database use case.

**Internal dashboards.** Dashboards, admin panels, utilities. Nobody's publishing high-volume content there. If your product/project is mostly visual without the need for classical content, skip the CMS. Need it localized? You can handle localized strings in the repo for labels and screens that barely change.

**Sites that genuinely rarely change.** There's a specific kind of digital brochure webpage (corporate sites, small business sites, F&B outlet sites, solopreneurs in some fields) that gets built once and then sits untouched for a super long time. The problem isn't tooling, it's that nobody's updating it. No one needs a CMS there if they've got some technical skills if I'm being honest.

**Prototypes and throwaway projects.** Stop adding infrastructure to things that might not exist in six months until its validated. We all know the meme.

(Image content)

---

So there. The framing that AI building and a CMS are competing choices is very off. Vibe coding accelerates the build and the CMS manages the content after the build.

If anything, vibe coding makes CMS integration *faster and smoother and better*. Your IDE can sort out a DatoCMS boilerplate and integrate everything within minutes to let your editors go do their thing.

What AI can't do is give Susan in marketing a usable interface to update the homepage without writing up a Jira ticket to distract you from whatever you were meant to be doing.

AND I KNOW WHAT YOU'RE THINKING NOW. "Why don't I just build a CMS, its just CRUD?". Just. No. But more on that soon.

---

# Why we're deliberately (still) totally not an AI-first Headless CMS

Source [blog]: https://www.datocms.com/blog/not-ai-first-headless-cms.md

Posted on [date: 2026-03-05T15:10:43.536+01:00] by Ronak Ganatra

### TLDR

-   Every CMS we're seeing seems to have gone into AI-first mode. We still haven't.
-   We're not "anti-AI" by any means, we're very AI-friendly: we use Claude Code internally, our users build dope things with Claude Skills and Cowork automations, and we have actual AI tooling available for those who want to use it. The key word being *want* to use.
    
-   We just won't ship features that sit unused and quietly route your content to an LLM you never opted into just to add a checkbox on feature bloat. Are we leaving money on the table with that decision? Most likely.
-   As a European company we still have *some* concerns about what we push through LLMs ourselves, so we're not really comfortable acting all willy-nilly with your project's data.
    

### Wait, are you not building AI features?

If you've opened almost any CMS dashboard recently, you've probably seen an AI writing assistant, an AI SEO optimizer, an AI image tagger, an AI-agent mode, an AI workflow builder, an AI dishwasher manual, and if you're especially lucky, a full-screen modal asking if you'd like to "superdupercharge your content strategy to the moon for ultra enterprise scale with AI to unlock generational efficiency and velocity."

(Video content)

You clicked past all of it and went back to work.

We know that because that's what you've been telling us.

But some of you on the other side of the discussion are also curious why we aren't shipping more AI features.

The TLDR of that is we've been watching, thinking, and deliberately not shipping native features for things that aren't ready, or aren't applicable enough to the vast majority of you.

On a high-level, DatoCMS and every Headless CMS is already AI-friendly by default. Our APIs and CLI are structured, clean, and straightforward enough that plugging them into any AI workflow is smooth. You're not fighting the CMS to make it work with your tooling. The content is already there, already structured, already query-able. We just haven't found the need to add an AI layer on top, and we decided not to drop noise so as to not get in the way of your work.

To be clear, we're not anti-AI. We just think there's a meaningful and practical difference between AI that's genuinely going to be used by most of you in your daily workflows, and AI that's been slapped into the CMS because the "industry demands it". We'd rather ship the former, even if it means shipping less.

That being said we're also not sitting on our hands either. We've been pushing out AI-related things, just not as core features you're forced to interact with. Everything we've done on that front is opt-in, for the use cases where it actually makes sense for you where you're free to use it if you want, and not forced to ignore it and work around it because a few others wanted it.

### What the industry shipped vs. what users actually think

Honestly though, AI in content tools is objectively a great idea. There are tons of great things you can do here, and we don't doubt that. The problem isn't the vision, it's the execution speed and the incentive to ship something rather than ship something good.

To go beyond our bias of our own opinions and users, we asked for a few insights in a few developer and marketing communities on what they actually thought of the AI features in the CMS they use, and here's some of of that.

> ...just really noisy but not useful...

> ...most of it boils down to generating text and I don't always use the CMS to create or write, I bring it from other tools and then edit...

> ...even for features that make a lot of sense such as polishing things in SEO terms, you'd get mostly a wall of text instead of having it perhaps a bit more integrated into the platform...

> ...all the great ideas are there, there is a V12 engine underneath, but it's being driven in the backyard...

That's a reflection of what we've been trying to avoid.

So what have we actually shipped?

Nothing native in the CMS that impacts ALL users (*I mean, aside from arguably the most useful native AI feature right now for me which is an emoji recommendation picker. I'm at peace with this* 💁‍♂️)

The philosophy behind everything this is that our AI features should be opt-in. None of it is baked into your core experience. None of it turns on without you going looking for it.

### We're far from "anti-AI"

This is worth being clear about, because "we haven't shipped a dozen AI features" can read as us just being petty haters. It's not meant to be.

We [released an MCP server](https://www.datocms.com/blog/we-have-released-an-mcp-sometimes-it-works.md) that lets you automate the heck out of your project to your heart's content. Rather than dumping 150+ API endpoints at an LLM and hoping for the best, we designed it with a layered approach that actually fits how models reason. It's there if you want to build AI-powered workflows on top of your content. Completely ignorable if you don't care about it.

We've put some solid elbow grease and [real work into making our documentation AI-friendly](https://www.datocms.com/blog/llms-txt.md), because you increasingly use LLMs to help you build things, and we'd rather that experience be genuinely useful for you than hallucination-prone. You can even go to any docs page and generate an `.md` or open it in GPT/Claude to start with instant context.

And we have [our AI translations plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md) for teams managing multilingual content. One job. Does it well. Optional. Don't install it if you don't need it.

We've taken a conservative and pragmatic approach to AI-enabled features, and we'll keep [updating our page as we add more](https://www.datocms.com/features/ai.md). We want to enable you to use AI well, if you want to use it. We don't want to make that decision for you.

And, as a matter of fact, we're constantly thinking of better and new AI features to implement, but we're making an explicit differentiation on what is AI for developer workflows (MCP, schema related things, CLI workflows, etc.), vs. what is "shiny" to add in like an agentic modal that mostly seems to go ignored.

If you have any other specific AI use case that you really wish was baked into the CMS for your project, [our plugins make that a lot more achievable than you might think](https://www.datocms.com/marketplace/plugins.md). Build one for your own project and make it public or keep it private, or reach out to us because we'd love to collab on making your vision happen and potentially find a way to ship a slightly more agnostic version of its' capability for the broader community if that's relevant to more users. The CMS doesn't need to make one AI bet for everyone when you can build exactly the thing you actually need.

Aside from features that face you, there's also internal considerations and wider discussions on the topic.

We use tools like Claude Code internally. We've watched our users and customers build genuinely cool workflows using Claude Skills and Cowork automations. The technology works. And it works well WITH DatoCMS. It's just that it works best when the person using it has chosen to reach for it, not when it's been decided for them.

There's also something we don't talk about enough as a European company: most AI features in SaaS products mean your content is being routed to LLMs with data residency and security policies that range from "non-EU" to "Trust me Bro."

For a lot of our customers, especially enterprise ones dealing with sensitive or regulated content, that's not a theoretical concern. We're not going to quietly make that call on your behalf by baking AI processing into core functionality.

The question we keep asking is whether someone would actually use a feature without being prompted to? Does it realllllly feel like a part of the product, or like something that got sloppily slapped on? And can we be genuinely transparent about what happens to your data when it runs?

(Image content)

I can't remember where it was found to credit it, most likely somewhere in /r/programmerhumor 😬

When it does, and when we can build it right, we will, and we'll keep you in the loop on things as they develop.

---

# Introducing Visual Editing. Click it. Change it.

Source [blog]: https://www.datocms.com/blog/introducing-visual-editing.md

Posted on [date: 2026-02-10T10:31:28.098+01:00] by Stefano Verna

### TL;DR

-   Visual Editing lets content editors click directly on any element of your website to edit it in DatoCMS.
-   Combined with draft mode and real-time updates, making changes is a breeze. No more hunting through record forms to find the right field.
    
-   Available on every plan (including Free!), with SDKs for Next.js, Astro, Svelte, and Vue.
-   The best part? Side-by-side editing is an upgrade to the existing Web Previews plugin, so there's minimal config required from your side to enable it for your editors.
    

## 👋 Hello, Visual Editing

Your editors don't think in models and fields — they think in pages and posts. And yet every time they need to change a headline, they're playing detective: opening tabs, scrolling through record forms, deciphering which "SEO title override" lives inside which modular block. It's a bad game and nobody's winning.

What if they could just… click the thing on the page and edit it?

(Video content)

Visual Editing supports two workflows. Use either one, or both. Editors pick whichever fits the moment:

### Click-to-edit: Content Link on your website

This is the simplest setup (especially if you've been using Vercel's integration with Headless CMS in the past). Editors visit your website in draft mode, hover over content to see what's editable, and click to open DatoCMS in a new tab. It works entirely on your frontend, and with any hosting: Vercel, Netlify, Cloudflare, you name it.

(Video content)

This alone is already a massive improvement. Suddenly, "yo where's that field, again?" becomes redundant.

### Visual Mode: Side-by-side editing in the CMS

So. We took the [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) plugin, which is loved by so many of you, and gave it some Rare Candy. The result is the workflow editors have been asking for since headless CMSes became a thing: preview on the left, edit panel on the right. Click on any piece of content, the edit panel opens right there. No tab switching, no context loss, instant live updates.

(Video content)

This plugin also enables you to have preview links in the CMS sidebar, have bidirectional navigation (scroll either panel, the other panel will keep up with context), AND give you full-screen Visual Editing mode. 🤯

**Want to see it in action before touching any code?** Head over to [try.datocms.com](https://try.datocms.com/). No registration, no login. You'll land in a fully set up project where you can experiment with Visual Editing right away.

## How it all works

Okidoki, let's dive into the fun bits of what's behind the scenes.

The key trick is steganography: invisible Unicode characters appended to every string in your GraphQL API responses. These characters encode each value's origin (record ID, field path, locale) so the frontend knows exactly where every piece of text comes from.

This means developers don't have to manually wire up every piece of content to its source field (which would be tedious at best, and a maintenance nightmare at worst). Instead, when you fetch draft content with Content Link enabled, the metadata just comes along for the ride. The `<ContentLink />` component scans the page for it and renders edit overlays on the right DOM nodes automatically.

---

## Getting started

The setup is minimal, and breaks down into three steps:

1.  **Enable stega encoding** by adding two headers to your existing GraphQL requests when fetching draft content:
    
    -   `X-Visual-Editing: v1`
        
    -   `X-Base-Editing-Url: https://<YOUR-PROJECT-NAME>.admin.datocms.com`
        
2.  **Add the** **`<ContentLink />`** **component** to your layout. It automatically scans the page for embedded metadata and renders edit overlays on the right DOM nodes — no manual wiring needed.
    
3.  **Optionally, install and configure the** [(Image content)Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) **plugin** to unlock side-by-side editing directly in the CMS.
    

That's it for the basics. It works with your whole content model out of the box: links to records, blocks, Structured Text, modular content. Some complex field types may need a couple of `data` attributes to help Content Link distinguish between editable regions, but it's minimal work.

#### Docs, Starters, SDKs & Integration Guides

We have dedicated SDKs and in-depth integration guides for [React/Next.js](https://www.datocms.com/docs/next-js/visual-editing.md), [Astro](https://www.datocms.com/docs/astro/visual-editing.md), [Svelte/SvelteKit](https://www.datocms.com/docs/svelte/visual-editing.md), and [Vue/Nuxt](https://www.datocms.com/docs/nuxt/visual-editing.md). Each provides its own `<ContentLink />` component (or equivalent) that handles detection, overlay rendering, and keyboard shortcuts. Drop it into your layout, and you're done.

Want a head start? Clone one of our [starter kits](https://www.datocms.com/marketplace/starters.md) — they come pre-configured with Draft Mode, Real-time Updates, Content Link, and Web Previews already wired together!

Using a framework we don't cover yet? All our SDKs are built on [`@datocms/content-link`](https://github.com/datocms/content-link), a framework-agnostic library you can integrate with directly.

---

Visual Editing is available on every plan. Including Free. No upgrade, no feature gate, no catch.

Now here's what we want from you: **break it**. Throw your gnarliest component architecture at it. Nested modular blocks three levels deep? Localized Structured Text inside a tabbed layout? We want to see it all. The weirder your setup, the more useful your feedback.

Jump in at [try.datocms.com](https://try.datocms.com/), and tell us how it went on the [community forum](https://community.datocms.com/). We're listening!

---

# Introducing our 2026 pricing changes: more value, less problems

Source [blog]: https://www.datocms.com/blog/introducing-our-2026-pricing-changes.md

Posted on [date: 2026-02-03T09:57:06.905+01:00] by Matteo Giaccone

We are rolling out an update to our Professional plan that should tackle some of the most frequent issues that we've seen reported, while also trying to solve some of our internal issues to keep DatoCMS performant for everyone.

Overall, this is a plan adjustment to **reduce the costs of what you cannot control** (like end-user traffic), while reducing some of the compute-intensive features that should be used with more care.

Let's start from the good news!

### Video: big price drop!

The biggest change is the big decrease of video costs. Thanks to the increasing usage of video across the entire user base, we are able to get a better agreement with [Mux](https://www.datocms.com/tech-partners/mux.md), and we are passing on the savings to you!

The Professional plan will **include 50K streaming minutes (up from 9K!)**, and additional packets will give you **12K minutes instead of 9K**, always for €9.

Another good news is that we are **removing video encoding from the equation** altogether. So no extra costs for video storage and encoding, it's on the house! 🤝

### API calls: CDA calls are 10x cheaper than CMA calls

Our philosophy has always been to try to keep things as simple as possible. To do that, we had one simple cost for API calls, both CDA and CMA. While this was easier to understand, it was a big compromise as CMA calls for us are much more expensive than CDA calls. Why? We cannot cache the former, while we are able to cache the read-only GraphQL calls of the CDA way better.

So we are now introducing a **split in costs of CDA and CMA calls**. The bad news is that CMA calls will become slightly more expensive, with extra packets at **€9 for 100K calls**. But the good news is that you will get **1 MILLION calls for €9 when using the CDA**!

**⚠️ Unfortunately, this change must be applied to existing plans as well**.

For standard plans we are going to move the current allowance to CDA and add 10% for CMA. For custom plans we tried to minimize overages checking the last 6 months of usage, if you have any doubts contact support, we want to help by reducing overages and getting more predictable costs, we know that in the long run is going to be better for both of us.

CDA is where most of the overages are normally generated, so you should be able to get good discounts here if you want to move to the new plan.

But, **if you want to stay on your current plan overages will be billed as they have been before**.

## Models, blocks, locales: what's changing

We have a recurrent issue with customers hitting the limit of block per record. This is frustrating for our users as they often need to change their approach to the content while having already committed a lot. It's not impossible but it's a bit of manual work that we cause for you.

So to try and minimize the issue we are going to **increase the number of models included in the plans from 60 to 100**. Since blocks are free while models are paid, we try to encourage you to use models instead, which scale better with links.

A bad news is around locales instead. Since blocks are multiplied by locales (normally), we are going to **reduce the number of included locales down to 5**. This is still very competitive, especially since DatoCMS is treating locales as first-class citizens, by validating content by locale, giving custom permissions and publishing state.

The block limit is adjusted to **500 blocks**, calculated as approximately 100 blocks per locale. We're also improving our CMS warnings to help you stay within the limits or to contact us as soon as we can guess it might be problematic.

## Bad news: environments and projects

Unfortunately we have one straight-up bad news: environments.

We have been very generous in the past by giving 8 environments included in each project. But things have escalated and we are now reducing this to provide a sustainable and fast platform for all users.

The fork of an environment is a very heavy operation that duplicates all the entries of your project. It's very close to a project duplication. Unfortunately, we realized that this was too much technical and performance complexity affecting the platform for all users, so we are **reducing the number of environments from 8 to 3**, while giving you the option to buy extra ones at €39/month. With the same rationale we are increasing the cost of extra projects within the subscription at €39/month.

## What About Existing Plans?

As always, **your current plan remains unchanged**, with one exception: the CDA/CMA split. Unfortunately we cannot avoid this change, but we are adding 10% more calls to existing plans, so it should be a net positive.

If you want to upgrade to the new plan, you should be able to do it on your own from the dashboard.

If you need any help to change plan or if you have any doubts around the details, just write to us at [support@datocms.com](mailto:support@datocms.com)

---

# A look back at 2025

Source [blog]: https://www.datocms.com/blog/a-look-back-at-2025.md

Posted on [date: 2025-12-22T12:09:21.097+01:00] by Stefano Verna

As 2025 comes to a close, it's once again time to reflect. It’s been another packed twelve months, and it’s great to look back at everything we achieved, day by day. (Yes, we're patting ourselves on the back. It's our blog, we're allowed to.)

Want to take a walk down memory lane? Here are previous editions: [2024](https://www.datocms.com/blog/a-look-back-at-2024.md), [2023](https://www.datocms.com/blog/a-look-back-at-2023.md), [2022](https://www.datocms.com/blog/a-look-back-at-2022.md), [2021](https://www.datocms.com/blog/a-look-back-at-2021.md), [2020](https://www.datocms.com/blog/a-year-in-review.md).

---

## Financials: **Strong growth, with best-in-class margins**

This year, we reached **€6.5 million in revenue**, a solid 10% year-over-year growth. Not that many companies still have double-digit growth after ten years! Most are either dead, laying off half their teams, acqui-hired, or pivoting to AI-something.

With our continued focus on sustainable operations and disciplined execution, we achieved an **EBIT margin of 65%**. To put this in perspective: while most SaaS companies celebrate 20-30% margins, and industry leaders hover around 40%, DatoCMS has reached a level of profitability that places us in the **top 5% of SaaS companies globally**.

For those familiar with SaaS metrics, the "Rule of 40" states that growth rate plus profit margin should exceed 40%. Ours is 75%. We're not bragging (okay, we're bragging a little) but it turns out that not burning through VC cash on ping-pong tables and "growth at all costs" actually works.

(Image content)

Annual Recurring Revenue

## Partners: More and more of you are joining us

With **185 agency partners** now fully enrolled in our [partner network](https://www.datocms.com/partners.md) (**!!!**), we're genuinely blown away. These are people who build websites for a living, with real deadlines and real clients breathing down their necks. They don't have time for tools that get in the way — and they chose us. We don't take that for granted.

This year, we doubled down on making your work more visible. All that real work for real clients? It adds up — we now have 340 projects in the showcase (**63 added this year alone!**), enough that we had to [revamp the page with proper filters](https://www.datocms.com/partners/showcase.md) so people can actually find things.

And what projects they are. You’ve used DatoCMS to [power offline wayfinding](https://www.datocms.com/casual-chats/on-powering-offline-wayfinding-for-printemps.md). You’ve helped [shape the early days of the entire GraphQL community](https://www.datocms.com/casual-chats/on-helping-shape-the-graphql-community-as-we-know-it.md). Heck, one of you even took a day to [graffiti the streets of Switzerland](https://www.linkedin.com/posts/datocms_probz-the-coolest-fn-instance-of-activity-7358481975231324162-UP4z) about us — which is either peak brand loyalty or a cry for help, we're not sure. Either way, never felt so loved.

If you're an agency and you're not in the [partner program](https://www.datocms.com/partner-program.md) yet — come on. We're not collecting logos here. We want to build a real relationship, learn what's slowing you down, and give you the perfect tool to ship quality work fast and painlessly. Half the features we shipped this year came from partner feedback. You're literally shaping the product. That's the whole point. No awkward sales calls, promise, we hate those too.

## Product: Another incredible round of improvements

2025 has been another year of **relentless shipping**. We didn't just focus on one area — we improved the entire stack, from the way developers write code to how editors manage content, all while hardening security and preparing for the AI era (gosh, we said it, now we need to wash our mouths).

Here is an exhaustive look at everything we shipped this year, grouped by how they help you:

###### Type Safety & Developer Confidence

-   [**Records, finally typed**](https://www.datocms.com/blog/records-finally-typed.md) — The biggest DX win of the year. The JavaScript client now supports full end-to-end type safety, generating types directly from your schema for real autocomplete and compile-time safety. No more `any` types haunting your dreams.
-   [**Reactive Plugins**](https://www.datocms.com/product-updates/reactive-plugins.md) — Plugin settings are now synced in real-time across users, preventing configuration conflicts when multiple people are working on complex setups simultaneously.
    

###### AI & LLM Readiness

-   [**LLM-Ready Documentation**](https://www.datocms.com/blog/llms-txt.md) — We made our docs AI-friendly with `llms-full.txt` and a "Copy as Markdown" feature on every page, so you can easily feed context to ChatGPT or Claude. Because let's be honest, that's how half of you read documentation now anyway.
-   [**MCP Server**](https://www.datocms.com/blog/we-have-released-an-mcp-sometimes-it-works.md) — We released a Model Context Protocol (MCP) server that enables AI assistants to interact directly with your DatoCMS projects. It works. Sometimes. We wrote a whole blog post about the "sometimes" part.
    
-   [**AI Translations**](https://www.datocms.com/docs/translating-content-with-ai.md) — Bulk-translate entire records with OpenAI, Claude, Gemini, or DeepL. Finally, a reason to stop copy-pasting into Google Translate.
-   [**Structured Text to Markdown**](https://www.datocms.com/product-updates/introducing-datocms-structured-text-to-markdown.md) — A new package that turns Structured Text fields back into clean, CommonMark-compatible Markdown: perfect for LLM pipelines or migration scripts.
    

###### Content Editing Experience

-   [**Inline Blocks in Structured Text**](https://www.datocms.com/product-updates/inline-blocks-are-landing-in-structured-texts.md) — One of our most requested features! You can now insert blocks directly inside Structured Text fields — perfect for inline links, mentions, or notes — unlocking infinite nesting possibilities.
-   [**Tabular View for Trees**](https://www.datocms.com/product-updates/trees-also-grow-on-datocms.md) — Hierarchical models got a massive upgrade with a new Tabular View, bringing custom columns, pagination, and sorting to tree structures.
    
-   [**Favorite Locales**](https://www.datocms.com/product-updates/favorite-locales-streamline-multilingual-editing.md) — Editors can now pin their most-used languages to the top of the UI, hiding the noise of unused locales in massive multi-language projects. Finally, some peace for the people managing 40+ locales.
-   [**Enhanced Previews**](https://www.datocms.com/product-updates/enhanced-previews-quickly-see-what-s-inside-your-blocks-and-links.md) — We introduced inline previews for blocks and link fields, letting you see colors, dates, and images directly in the list view without clicking through.
    
-   [**Single Block Presentation**](https://www.datocms.com/product-updates/single-block-fields-presentation-title-or-image.md) — You can now use a Single Block field as a model's presentation title or image, perfect for models where the main info is nested inside a block.
-   [**Improved Link Field Filtering**](https://www.datocms.com/product-updates/improved-link-field-filtering-by-locale.md) — Link fields now correctly filter records by the current locale, eliminating confusion when referencing localized content.
    
-   [**Fixed Headers**](https://www.datocms.com/product-updates/a-more-consistent-cms-experience-with-fixed-headers.md) — We unified the UI with fixed headers across all sections, ensuring that save and publish buttons are always within reach. A small change that sounds boring until you realize how much scrolling it saves.
    

###### API & Tooling Power

-   [**New CLI cma:call command**](https://www.datocms.com/product-updates/new-cli-command-to-call-any-datocms-api-method-directly.md) — You can now call *any* API method directly from the terminal without writing custom scripts, thanks to dynamic discovery of API resources.
-   [**Filter uploads by path**](https://www.datocms.com/product-updates/filter-uploads-by-path.md) — We added a new path filter to the GraphQL API, allowing you to query assets based on their storage path with inclusion, exclusion, and exact matching.
    
-   [**Increased GraphQL Pagination**](https://www.datocms.com/product-updates/increased-pagination-response-to-500-items.md) — We bumped the maximum number of items you can fetch in a single GraphQL query from 100 to 500, reducing the number of requests needed for large datasets. Five times more stuff in one go — you're welcome.
-   [**Site Search Decoupled**](https://www.datocms.com/product-updates/build-triggers-and-site-search-are-now-two-different-entities.md) — Site Search is now an independent entity, separate from Build Triggers. You can control indexing explicitly and access [**detailed crawler logs**](https://www.datocms.com/product-updates/site-search-crawler-logs-enhanced-visibility-into-indexing-issues.md) to debug robots.txt and sitemap issues.
    
-   [**Enhanced Build Triggers Activity**](https://www.datocms.com/product-updates/improved-build-triggers-activity-view.md) — We enhanced the Activity view to show events beyond the 30-item limit, with better filtering and detailed logs for every operation.
    

###### Security & Governance

-   [**Access to CDA Playground with Limited Permissions**](https://www.datocms.com/product-updates/access-to-cda-playground-with-limited-permissions.md) — Developers can now use the GraphQL Playground without needing full API token management permissions, safer for contractors and temporary access.
-   [**All API Tokens are Deletable**](https://www.datocms.com/product-updates/all-api-tokens-are-deletable-now.md) — For better security hygiene, you can now delete *any* API token, including the default read-only ones generated by the system.
    
-   [**API Token Last Used Time**](https://www.datocms.com/product-updates/see-last-used-time-for-api-tokens.md) — You can now see when each API token was last used directly in Project Settings, making it easy to identify stale tokens and clean up ones that haven't been active in months. Or years. We don't judge.
-   [**No Default Full-Access Token**](https://www.datocms.com/product-updates/we-no-longer-create-a-default-full-access-api-token-for-new-projects.md) — New projects no longer come with a full-access API token by default, encouraging the principle of least privilege from day one.
    
-   [**Improved Roles & Permissions**](https://www.datocms.com/product-updates/improvements-to-managing-user-roles-and-content-permissions.md) — We revamped the roles interface to clearly show inherited permissions and human-readable summaries of what a user can actually do.
    

###### Workflow & Quality Control

-   [**DatoCMS Recipes & Import/Export**](https://www.google.com/search?q=https://www.datocms.com/product-updates/introducing-datocms-recipes) — We launched a marketplace of reusable project "recipes" — pre-built models and blocks you can install into any project to save setup time, powered by the new [Schema Import/Export plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-schema-import-export.md).
-   [**Dedicated SEO Fallback Options**](https://www.datocms.com/product-updates/fine-tune-your-seo-with-new-dedicated-fallback-options.md) — We decoupled SEO metadata from internal preview fields, allowing you to set specific fallbacks for SEO titles and images without affecting the CMS UI.
    
-   [**Force Validations on Publishing**](https://www.datocms.com/product-updates/force-validations-on-records-when-publishing.md) — You can now prevent the publishing of records that don't meet current validation rules — crucial when you've tightened schema requirements on existing content.
-   [**Save Invalid Drafts**](https://www.google.com/search?q=https://www.datocms.com/product-updates/save-invalid-drafts) — Conversely, you can now save *drafts* even if they are invalid, allowing editors to save their work-in-progress without being blocked by strict validation rules until they are ready to publish. Because sometimes "half-done" is better than "lost."
    
-   [**Draft Mode by Default**](https://www.google.com/search?q=https://www.datocms.com/product-updates/draft-mode-active-as-default-for-models) — To encourage better editorial workflows, "Draft/Published" mode is now the default setting for all new models.
-   [**Smart Confirmation Guardrails**](https://www.datocms.com/product-updates/smart-confirmation-guardrails.md) — Destructive actions now calculate their impact before execution. If you're about to delete something used in 10+ records, we force a typed confirmation to prevent accidents. We've all been there. This is us protecting you from yourself.
    

...and we also cleaned up some tech debt by [sunsetting legacy batch endpoints](https://www.datocms.com/product-updates/deprecated-batch-operations-endpoints-will-stop-working.md) and [removing unused CI triggers](https://www.datocms.com/product-updates/no-new-travis-ci-and-circleci-build-triggers.md), keeping the platform lean and fast.

## Plugins: The ecosystem keeps growing

**30 new public plugins** landed in the [marketplace](https://www.datocms.com/marketplace/plugins.md) this year — plus countless private ones we'll never see. The community (and our support team!) keeps surprising us with stuff we didn't even know we needed.

-   [(Image content)AI Translations](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md) — Bulk-translate entire records with OpenAI, Claude, Gemini, or DeepL. Finally, a reason to stop copy-pasting into Google Translate.
-   [(Image content)Schema Import/Export](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-schema-import-export.md) — Move models between projects without losing your mind. The backbone of our new [Recipes](https://www.datocms.com/blog/schema-import-exports-and-recipes.md) feature.
    
-   [(Image content)Asset Optimization](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-asset-optimization.md) — Mass-optimize your media library and watch your storage bill shrink.
-   [(Image content)Custom Text Styles](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-custom-text-styles.md) — Add custom marks and styles to Structured Text. Your designers will love you.
    
-   [(Image content)Phone Number](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-phone-number.md), [(Image content)Zoned DateTime Picker](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-zoned-datetime-picker.md), [(Image content)Bulk Change Author](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-bulk-change-author.md) — Small tools that solve real annoyances.
    

## Infrastructure: The journey to independence

This year, DatoCMS handled an average of **3.5B API calls/month** (+80%), while serving **500TB of traffic/month** and **4.5M optimized video views/month**. At the same time, we executed the most ambitious engineering project in our history: **a complete migration from Heroku to a custom Kubernetes cluster on AWS**.

For almost ten years, managed hosting served us well — but by mid-2024, we had hit a ceiling. Costs were rising while our need for granular control grew. We realized we were paying a premium for convenience we no longer needed. It was time to build our own home.

The journey began back in October 2024, kicking off a **nine-month marathon**. We spent the winter prototyping (experimenting with everything from bare metal to alternative PaaS providers — some of which shall remain unnamed to protect the guilty), the spring architecting, and the early summer stress-testing.

After months of planning, we flipped the switch on Saturday, June 7th. We prepared for a battle, but we mostly ended up watching dashboards. Aside from a tiny detail that cost us exactly **1 minute** of downtime, the transition was flawless. By the time we turned the writes back on, every byte of data had been successfully secured in AWS.

The results were immediate and startling:

-   **Speed:** Response times for the Content Delivery API (CDA) were **halved** instantly.
-   **Efficiency:** We are now running on **64GB RAM** database instances on AWS that handle traffic better than the **256GB** instances we used on Heroku. Yes, you read that right. Four times less RAM, better performance.
    

It was a massive bet, but looking at the metrics today, it is undeniably one of the best wins of our year.

(Image content)

Response time, before and after the switch

We didn't just move servers and DBs; while moving our core applications to **AWS EKS** was the main event, we executed a total overhaul of the ecosystem surrounding it:

-   **Infrastructure as Code:** We codified our entire environment using **Terraform**, giving us a reproducible, version-controlled blueprint of our infrastructure that eliminates manual configuration drift.
-   **CDN Caching:** We switched from Fastly to **Cloudflare** for our CDN cache, implementing smarter caching rules that improved our hit ratio from 85% to 97%.
    
-   **Storage:** We migrated from AWS S3 to **Cloudflare R2**, eliminating massive egress fees and optimizing asset delivery. Goodbye, AWS data transfer bills. We won't miss you.
-   **Observability:** We ditched expensive CloudWatch logs for a custom **Prometheus & Loki** stack, slashing our monitoring bills to near zero while improving data quality.
    
-   **Developer Experience:** To tame Kubernetes complexity, we built **cubo**, a custom `kubectl` wrapper tailored around our needs that handles everything from generating K8S manifests and orchestrating rollouts to managing cronjobs, real-time logs, and one-off commands, preserving the "git push" and CLI simplicity we loved on Heroku.
    

(Image content)

If you want to know more about cubo, just ask! :)

**The Bottom Line:** We lowered overall infrastructure costs by **over 25%**, reduced Content Delivery API latency by 50%, expanded Realtime API capacity by **10×**, and gained full control across every infrastructure layer. And we kept our sanity. Mostly.

## Beyond code: Taking control of the books

While liberating ourselves from managed hosting, we made another quiet move: **we fully internalized our accounting**. For years, we outsourced this to external firms — the typical setup where you hand over receipts and hope for the best. But as we grew, flying blind between quarterly reports became untenable.

Now we run everything in-house with full visibility into our finances at any moment. No more waiting for external accountants to reconcile things. Same philosophy as the infrastructure migration: **control beats convenience** when you're building for the long term.

## Team: Still small by design

This year marked our **10th anniversary** — a decade of surviving frontend trends, CMS wars, and the occasional existential crisis about whether "headless" is still a cool term. To celebrate, we flew our entire team to the Tuscan countryside to eat, drink, and ride quad bikes. You can read the full story of our trip (and our "25% Matteo concentration rate") here: [**Dato Turns 10**](https://www.datocms.com/blog/dato-turns-ten.md).

(Image content)

(Image content)

(Image content)

(Image content)

(Image content)

Despite our growth in revenue and traffic, we remain a team of just **13 people**. This isn't an accident — it's a deliberate choice.

As we wrote in [**"How can you be eight people?"**](https://www.datocms.com/blog/how-can-you-be-eight-people.md) (well, now thirteen), building a massive organization is optional. We choose to ignore the pressure to maximize headcount or chase VC funding. Instead, we focus on what actually matters: a solid product, a healthy work-life balance, and staying profitable on our own terms. We don't mind "leaving a little water in the cloth" if it means we get to keep building the software we love, the way we want to build it.

## What's next?

No idea. And honestly, we like it that way.

We're not going to pretend we have a five-year vision carved in stone or a slide deck about "the future of content." We'll keep shipping what matters, keep ignoring the hype cycles, and keep cashing checks instead of burning through runway.

That said... **we** ***may*** **have a few things cooking** that we're genuinely excited about. But we're not going to jinx it by overpromising — you'll see them when they ship.

Well, see you in 2026. We'll still be here. Probably still 13 people. Definitely still not taking ourselves too seriously. 🧡

---

# Using a Headless CMS to index your content for LLMs

Source [blog]: https://www.datocms.com/blog/headless-cms-for-llms.md

Posted on [date: 2025-12-19T15:45:52.591+01:00] by Ronak Ganatra

## TLDR

-   `llms.txt` is a simple proposal: slap a Markdown index at `/llms.txt`, and expose clean `.md` versions of useful pages so LLM tools do not have to scrape your HTML.
-   [THIS IS A STANDARD PROPOSAL](https://llmstxt.org/). It's not widely confirmed to be "the way" llms are learning from websites, but since all the cool kids are doing it, we went ahead with our own approach to generating them.
    
-   With DatoCMS as your content source, you can generate `/llms.txt`, `/llms-full.txt`, and per-page `.md` exports at build time in any framework. We like Next.js and Astro, so we've put in some examples.
-   We recently released a package that converts Structured Text back into Markdown, so we're making things way easier for you.
    
-   We use this approach ourselves to generate `datocms.com/docs/llms-full.txt`. Did it help 10000x our "GEO" traffic. No. But it was fun.
    

### Let's talk LLM crawling

Ok first, let's get the whole buzzword bingo out of the way before I drive myself up a wall because I'll want to avoid stuffing these terms everywhere going foward.

There's a proposal going around to standardize having a .md version of websites for LLMs to consume. Just like `robots.txt` and `sitemap.xml`, `llms.txt` is a new standard proposal for how LLMs should/could/would learn about your website content, just like the Big G uses the others to crawl your URLs to index on SERPs. The full docs on this are on [https://llmstxt.org/](https://llmstxt.org/)

Also, in the very noisy world of Marketing we're all reading about how [GEO is taking over SEO](https://www.seo.com/ai/geo-vs-seo/) and how you're leaving billions on the table by not optimising your content for LLMs. Hype? Maybe. Worth listening to? Also maybe. Are we doing anything about it yet? Honestly, *not much*.

Anyways.

---

HTML is for browsers.

LLMs can read HTML, sure. But they also have to wade through your navigation, footers, cookie banners, repeated “Try for free” buttons, newsletter signups, styling, random layout text, and whatever else your frontend framework produced that day.

If you have ever pasted a docs page into ChatGPT and watched it hallucinate a method that does not exist, you have met the consequences of bad context.

So instead of letting AI tools scrape your UI, we give them the actual content, in a format they *alledgedly* like. Good 'ol markdown.

The proposal is straightforward:

1.  Publish a Markdown file at `/llms.txt` that gives background, guidance, and links to the good stuff.
    
2.  Also provide a clean Markdown version of pages at the same URL with `.md` appended (and if the URL has no filename, you append `index.html.md`).
    

That is it. No magic. No “new crawler standard”. Just a sane convention so tooling can reliably find high-signal content without playing DOM archaeologist.

If you want the “give me everything” version, the ecosystem has drifted toward a second artifact: `llms-full.txt`. Same idea, but it is one big compiled Markdown file. We shipped that for our docs too. You can find it on [https://www.datocms.com/docs/llms-full.txt](https://www.datocms.com/docs/llms-full.txt)

Why does Markdown work better though? This is not “Markdown is prettier”. It is “Markdown is predictable”.

Markdown preserves structure with minimal noise: headings, lists, code blocks, quotes. The llms.txt proposal explicitly calls out that Markdown is both human and LLM readable, and also consistent enough for deterministic processing.

HTML can represent structure too, but it also represents your layout. LLM tools do not care about your layout. They care about the content and its relationships.

### LLM indexing today

From everything I could find when I was researching this topic for us, there seem to be 2 lanes, and they constantly get mixed up.

**Lane 1: Training-time web data**

Some model training datasets are derived from the great big web crawls. [Common Crawl](https://commoncrawl.org/) exists specifically to provide large-scale web crawl data, and it is widely used in research and industry. There are also well-known filtered datasets built from Common Crawl, like C4 (Colossal Clean Crawled Corpus).

Do I know anything about what these mean? No.

You do not control what gets used, when, or how it’s filtered. Also, even if a page is crawled, that does not mean it ends up in a specific model’s training data, or stays there forever.

**Lane 2: Inference-time retrieval and ingesting**

This is the one you *actually* should care about day to day.

Tools like Claude, Custom GPTs, Cursor, and coding assistants ingest a source, index it, and retrieve relevant chunks when you ask questions. Our own `llms-full.txt` post is basically a love letter to this workflow, because it turns “open 10 tabs and copy paste like a maniac” into “one file, full context, here you go, gobble it all up.

Also yes, vendors run crawlers too. OpenAI documents its crawlers and how site owners can manage them, and Anthropic documents its bots as well.

But again, the easiest win is still to just publish better context.

**OK BUT ENOUGH THEORY. VAMOS, LETS GET TO THE FUN STUFF!**

### **GENERATING LLM exports from DatoCMS**

Can you use your Headless CMS project as your source of truth to generate these files on the frontend? Yes.

Can you turn Structured Text into markdown? Also Yes. Have you met the all new [structured-text-to-markdown](https://www.datocms.com/product-updates/introducing-datocms-structured-text-to-markdown.md)?

Can your repo zip up clean .md files at build time with all the new content added in? Also yes. Let's play around with how you can do that in Next.js and Astro.

The architecture is boring, which is good:

-   Content lives in DatoCMS.
-   Your site renders normally.
    
-   At build time, you generate:
    
    -   `/llms.txt` as an index
        
    -   `/llms-full.txt` as the “everything dump”
        
    -   optional per-page `.md` endpoints for deep links like we do.
        

But first, if you've got a project using Structured Text, let's get that one little rendering hiccup out of the way so you don't have to write your own logic.

Install the package which converts Structured Text nodes back into CommonMark-compatible Markdown, including headings, lists, code blocks, links, and formatting.

Terminal window

```bash
npm install datocms/structured-text-to-markdown
```

And now your pipeline can:

-   fetch records
-   convert Structured Text field outputs to Markdown
    
-   stitch outputs into `llms-full.txt` or per-page `.md`
    

Without you writing a custom renderer that breaks the first time someone pastes a table, or whatever.

#### Playing around in Next.js

[Next.js App Router route handlers](https://nextjs.org/docs/app/getting-started/route-handlers) are perfect for this, because they are just Web `Request` and `Response` handlers, living wherever you want in `app/`.

One important detail though, GET route handlers are not cached by default. If you want this to behave like a build artifact, opt into caching with `export const dynamic = 'force-static'` (or another caching strategy).

So, you could have a simple setup for a `app/docs/llms-full.txt/route.ts`

```typescript
export const dynamic = "force-static";

export async function GET() {
  // 1. Fetch docs records from DatoCMS
  // 2. Convert Structured Text to Markdown
  // 3. Join all this into one big .md boi

  const body = [
    "# DatoCMS Docs",
    "",
    "This is a compiled export of all our docs.",
    "",
    "## Getting started",
    "",
    "...",
  ].join("\n");

  return new Response(body, {
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
}
```

For `/docs/llms.txt`, you do the same, but generate a smaller index that links out to your important `.md` pages. If you want per-page `.md`, you can add routes that map slugs to Markdown output. The proposal explicitly recommends the `.md` suffix convention for “clean Markdown version of this page".

#### Playing around in Astro

Astro is almost annoyingly perfectly suited to something like this. Why? Endpoints can emit plain text, and if your site is statically generated, Astro will freshly bake that file at build time.

`The setup is also extremely simple and straightforward:`

```typescript
import type { APIRoute } from "astro";

export const GET: APIRoute = async () => {
  // 1. Fetch docs records from DatoCMS
  // 2. Convert Structured Text to Markdown
  // 3. Join all this into one big .md boi

  const body = [
    "# Docs",
    "",
    "This is a compiled export of all our docs.",
    "",
    "## Getting started",
    "",
    "...",
  ].join("\n");

  return new Response(body, {
    headers: { "content-type": "text/plain; charset=utf-8" },
  });
};
```

Astro also documents the convention that the filename determines the output path, so `llms-full.txt.ts` becomes `/llms-full.txt`. Clean and obvious.

## The lazy conclusion

If you want LLMs to help you build, you have to give them context that does not suck.

Use a headless CMS to manage the content properly. Then publish an LLM-friendly Markdown representation at build time, using the conventions tools are starting to align on. That is the whole trick.

Also yes, this is us doing the hard work so you can be lazy later 💁

And if you're intimidated by this because it's new and complicated, don't be. I'm not even a developer and I managed to vibe-get this done (which of course, boss man nuked and re-did properly, but hey, my thing still worked 😑)

(Image content)

---

# We have released an MCP, sometimes it works

Source [blog]: https://www.datocms.com/blog/we-have-released-an-mcp-sometimes-it-works.md

Posted on [date: 2025-11-28T10:29:08.115+01:00] by Stefano Verna

**TL;DR:** We're releasing the DatoCMS MCP after six months of development. The MCP ecosystem is flooded with low-quality implementations that barely work. Ours is better — using a layered approach with some carefully designed tools instead of dumping 150+ API endpoints on the LLM. It's kinda slow, it burns tokens, and works sometimes. Given where LLM technology actually is today, "sometimes" might be the best anyone can achieve. [**Try it here!**](https://www.datocms.com/docs/mcp-server.md)

---

Every few years, tech discovers a new universal standard that will finally make everything talk to everything. This time it's MCP — **USB-C for AI** — and the industry has responded with the restraint and careful consideration you'd expect. Which is to say: [+6,000 implementations in under a year](https://www.mcpevals.io/blog/mcp-statistics), most held together with duct tape and wishful thinking.

Anthropic, OpenAI, Google, Microsoft... they're all in. And now every product manager on earth has the same slide in their deck: "MCP Integration Q2."

We spent six months building ours, genuinely excited about the possibilities it could offer. Three rewrites. Humbling amounts of testing. Today [**we're releasing it**](https://www.datocms.com/docs/mcp-server.md) — not because we've cracked the code, but because after seeing what passes for "working" in this ecosystem, we figured we'd throw our hat in. It works. Sometimes. Apparently that's above average.

## The uncomfortable truth about MCP quality

Let me be blunt: the average quality of MCPs that interact with SaaS products is **abysmal**. The hype has led to a flood of implementations that are low-quality, poorly documented, and rushed to market.

The most pervasive problem isn't even security — a serious topic I won't address here — it's that they are, very often, an **agonizing experience for the end user**.

A widely-shared critique documented that Claude Sonnet 3.7 achieved only a [16% success rate on airline booking tasks](https://blog.sshh.io/p/everything-wrong-with-mcp) — the very kind of multi-step workflows MCPs promise to enable. David Cramer, Sentry's co-founder and early adopter, stated flatly: ["MCP is not good, yet."](https://cra.mr/mcp-is-not-good-yet/) The consensus among those actually building with MCP? It's premature at best.

We tested a recently released MCP from a competitor's CMS. The result? An endless cascade of failed API calls, each one marked with a warning icon. The LLM had no clue how to make the API calls using the provided tools — it just kept trying random endpoints, guessing at parameter structures, repeating the same mistakes. It was like watching someone fumble in the dark. The end result? When asked to translate an article to Italian, it overwrote the English content instead.

This isn't an edge case — **this is the norm**. Why?

First, because most **MCPs are rushed to market** due to internal and external pressures — whether from the C-suite, prospects, or competitors. Companies are launching MCPs with vague tool descriptions, incomplete documentation, and zero consideration for whether the LLM can actually understand them.

Second, because **LLMs are still too dumb**. Companies thought they could just "wrap" their APIs in a thin layer of MCP and delegate the hard work to the LLMs... but unless we're talking about extremely simple products, that's just a fantasy. APIs are too hard for LLMs, especially if the MCP does not help by offering some kind of documentation or examples — which most don't.

Third, because the **protocol itself is flawed**. [Performance degrades after 40 tools and collapses after 60](https://demiliani.com/2025/09/04/model-context-protocol-and-the-too-many-tools-problem/) — not exactly "USB-C for AI" when plugging in more things breaks everything.

The best part? [Anthropic themselves](https://www.anthropic.com/engineering/code-execution-with-mcp) effectively admitted **the whole approach is broken**: tool definitions and intermediate results eat your context window alive, slowing agents, raising costs, and increasing errors. And just a couple of days ago, [they doubled down](https://www.anthropic.com/engineering/advanced-tool-use) formalizing three "official" workarounds for problems people were desperately trying to solve on their own. When the creator has to keep releasing patches to make their own protocol usable, maybe it's time to stop pretending this is the future of anything.

The bottom line: making MCPs that actually work is **hard**. You either have to do the heavy lifting yourself — simplifying, pre-processing, hand-holding the LLM — or limit your tools to a handful of very precise actions. Not exactly the "just plug it in and let AI do the rest" future we were promised.

So what did we do?

## The DatoCMS MCP: better than some (which isn't saying much)

Here's our entry: the DatoCMS MCP. Six months of development, three rewrites, and countless failed approaches. **We hit every wall Anthropic just documented.** The result? It's not perfect, but it's reliable enough to be useful. What makes ours different?

-   **10 tools, not 150**: DatoCMS offers 40+ resources and 150+ API endpoints. We've reduced this to just 10 tools by following a [layered approach](https://engineering.block.xyz/blog/build-mcp-tools-like-ogres-with-layers). Instead of dumping everything on the LLM and hoping for the best, we guide it through stages: first discover what's available, then plan the operation, then execute. The result? Fewer malformed calls, less confused reasoning, and workflows that are actually debuggable.
-   **Script-based approach**: Instead of making one API call at a time, the LLM can write complete TypeScript scripts that batch multiple operations together. These scripts are validated before execution to catch errors early. Batching reduces round-trips and token overhead, and gives the LLM full context to reason about the entire operation — not just isolated steps.
    
-   **Incremental editing**: When errors occur (and they will), instead of rewriting lengthy scripts repeatedly, our MCP allows precise modifications, significantly speeding up the trial-and-error process.
-   **Documentation-aware**: Unlike MCPs that just throw raw API endpoints at the LLM and expect the AI to sort it out, our MCP retrieves detailed information about each method and concrete examples from documentation. This consumes quite a few tokens, but it's *the only way* to give the LLM a fighting chance.
    

It's been quite funny to see Anthropic release, just days ago, [very similar solutions](https://www.anthropic.com/engineering/advanced-tool-use) to the exact problems we'd been wrestling with. (Or maybe not so funny, depending on your perspective.)

-   Their [Search Tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool) → our "Layered discovery"
-   Their [Programmatic Tool Calling](https://platform.claude.com/docs/en/agents-and-tools/tool-use/programmatic-tool-calling) → our "Script-based execution"
    
-   Their [Tool Use Examples](https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use#providing-tool-use-examples) → just... including documentation. We did that.
    

But here's the difference: theirs are generic features. Ours are **tailored to DatoCMS specifics** — our documentation is already packed with TypeScript examples, so feeding those to the LLM gives it far more context than generic "tool use examples" ever could. Plus — and this is a big plus — because our approach is baked into the MCP itself, **it works today across any client**: Claude Desktop, VS Code with Copilot, whatever. Not just Claude's API.

So what does this actually look like? The MCP can handle complex operations like generating landing pages, translating content, and modifying schemas — tasks that leave most competitors' implementations in shambles:

(Video content)

Pro tip: skip Claude Desktop. The UI lags like hell. Use Claude Code or Codex instead.

## The harsh reality

But let's not pretend this is a victory lap. Our MCP still has significant limitations:

-   **Token consumption:** Reading documentation is expensive.
-   **Speed**: Don't be surprised if operations take a while. A "simple" operation could finish in seconds or stretch to several minutes, depending on how the LLM approaches it.
    
-   **Unpredictability**: LLMs are not that smart. They forget things. They take absurd paths even when given all necessary information.
-   **Scale limitations**: It struggles with particularly large records and complex modifications.
    

The fundamental problem **isn't our implementation** — it's that LLMs aren't ready for this level of autonomy. They're statistical models that sometimes produce useful outputs.

So why release it at all? Because "sometimes" is the best anyone can achieve right now — and our "sometimes" is good enough to solve real problems. The encouraging part: once patterns for a task are established, **subsequent operations become more reliable**. Users who provide clear, precise prompts will get better results.

## The path forward

The MCP ecosystem is messy right now, but that doesn't mean the concept is doomed. It means we're in the early, awkward phase where standards are forming and best practices are emerging.

Our contribution is an MCP that actually works for complex multi-step operations. Yes, it's kinda slow. Yes, it burns tokens. But it can bulk-update SEO fields, scaffold new content types, migrate content between models... the tedious, multi-step operations you'd never want to do by hand.

**We've shipped it as a beta** because it's useful enough today and will only improve with real-world feedback. Are there simpler approaches on the horizon? Maybe — [Claude Skills](https://simonwillison.net/2025/Oct/16/claude-skills/) and others are worth watching. But this is what we've got now, and we think it's solid.

Give it a try! → [**datocms.com/docs/mcp-server**](https://www.datocms.com/docs/mcp-server.md)

---

# Your entire DatoCMS docs in one file: meet llms-full.txt

Source [blog]: https://www.datocms.com/blog/llms-txt.md

Posted on [date: 2025-10-21T16:25:20.619+02:00] by Stefano Verna

**TL;DR**

-   **We've shipped** [**llms-full.txt**](https://www.datocms.com/docs/llms-full.txt) — our complete documentation, all 500+ pages, in one clean Markdown file optimized for AI tools.
-   **Drop it into Claude Projects, custom GPTs, NotebookLM, Cursor, or any AI assistant** and get accurate, context-aware answers about DatoCMS without hunting through docs.
    
-   **It's the natural next step** after our [LLM-ready documentation export](https://www.datocms.com/product-updates/llm-ready-documentation-export-any-page-as-markdown.md) — now you get the *entire* knowledge base in one shot!
    

## Why this matters

A few weeks ago, we added the ability to [export any docs page as Markdown with a single click](https://www.datocms.com/product-updates/llm-ready-documentation-export-any-page-as-markdown.md). It works beautifully for single pages. But when you're building something real with DatoCMS, you don't just need one page. You need *context*. You need to understand how the pieces fit together.

Here's the thing about working with AI tools: they're only as good as the context you give them. Ask ChatGPT to help you build a content migration script without any docs? You'll get generic code that might corrupt your data. Paste in the full migration documentation? You get production-ready scripts with proper error handling and validation that actually work with your content model.

Before `llms-full.txt`, feeding complete context meant opening 10+ tabs, copy-pasting page after page, and hoping the AI could piece it together.

Now? One file. Full context. Every time.

That's `llms-full.txt` — our entire documentation compiled into a single, perfectly formatted Markdown file. API references, guides, migrations, plugins, CMA, CDA, all of it. One URL. One paste. Complete context.

## Here's how to actually use it

###### Claude Projects: Your DatoCMS expert on demand

[Claude Projects](https://www.anthropic.com/news/projects) let you attach custom knowledge to Claude. This is where llms-full.txt absolutely shines.

**Setup (takes 30 seconds):**

1.  Go to [claude.ai](https://claude.ai/) and create a new Project
    
2.  Drop the `llms-full.txt` file and use [these instructions](https://gist.github.com/stefanoverna/e6d225bc3eef2d11bdaae16fb433a5bd#file-datocms-expert-instructions-md) as your starting point
    
3.  Give it a name like "DatoCMS Docs"
    

**Now you can:**

-   Ask "How do I migrate from WordPress to DatoCMS?" and get step-by-step instructions
-   Say "Write a script to bulk-update all my blog posts" and get working TypeScript
    
-   Plan complex content model migrations with full awareness of DatoCMS capabilities
    

Claude remembers everything from `llms-full.txt` across every conversation in that Project. It's like having a DatoCMS architect in your back pocket.

###### Custom GPTs: Build your own DatoCMS assistant

Want a ChatGPT that *only* talks DatoCMS? You can build one in minutes.

**How to build it:**

1.  Go to [ChatGPT GPT Builder](https://chat.openai.com/gpts/editor)
    
2.  Click "Create a GPT"
    
3.  In the Knowledge section, upload `llms-full.txt` (download it first from [here](https://www-draft.datocms.com/docs/llms-full.txt.md))
    
4.  Give it instructions like: "You are a DatoCMS expert. Answer questions using only the provided documentation. Include code examples when relevant." You can use [these instructions](https://gist.github.com/stefanoverna/e6d225bc3eef2d11bdaae16fb433a5bd#file-datocms-expert-instructions-md) as your starting point.
    

**Pro tip:** Check out our [official DatoCMS Expert GPT](https://chatgpt.com/g/g-68f2397c654081918601c5fa11a21616-datocms-expert) to see what's possible!

###### NotebookLM: Research and learn DatoCMS

Google's [NotebookLM](https://notebooklm.google.com/) is brilliant for deep research and learning.

**Setup:**

1.  Create a new notebook in NotebookLM
    
2.  Add a source → paste `https://www.datocms.com/docs/llms-full.txt`
    
3.  Let it process (~30 seconds)
    

**Now you can:**

-   Ask "Compare the CMA and CDA APIs — when should I use each?"
-   Say "Create a study guide for learning DatoCMS migrations"
    
-   Request "Find all mentions of image optimization across the docs"
    

NotebookLM excels at understanding relationships across documentation. Perfect for onboarding new team members or exploring features you haven't used yet.

###### Cursor & Windsurf: Code with full docs context

AI coding assistants need context about the tools you're using. That's where llms-full.txt becomes essential.

**In Cursor:**

1.  Open Cursor and type `@Docs`
    
2.  Select "Add new doc"
    
3.  Paste: `https://www.datocms.com/docs/llms-full.txt`
    

**In Windsurf:**

1.  Open settings → Documentation
    
2.  Add new documentation source
    
3.  Enter the URL: `https://www.datocms.com/docs/llms-full.txt`
    

**Critical tip:** Always type the `@` symbol manually in the chat interface. Copy-pasting breaks the context reference.

**Now you can:**

-   Type "create a Next.js page that fetches blog posts from DatoCMS" and get working code
-   Ask "add pagination to this query" and it knows the DatoCMS pagination API
    
-   Debug GraphQL queries with full knowledge of available fields and filters
    

Your AI coding assistant now knows DatoCMS as well as you do. Maybe better.

## What makes it work

You might be thinking: "Can't I just scrape the docs?" Sure. But you'll get broken formatting, navigation menus mixed with content, and JavaScript cluttering everything.

llms-full.txt is different — clean Markdown with every code block formatted perfectly, logical structure, complete context across all 500+ pages, and it's regenerated automatically with every docs update. No noise, just the information that matters.

## Try it yourself

Here's everything you need:

-   **The file**: [`https://www.datocms.com/docs/llms-full.txt`](https://www.datocms.com/docs/llms-full.txt)
-   **Standard llms.txt**: [`https://www.datocms.com/docs/llms.txt`](https://www.datocms.com/docs/llms.txt) (just the index)
    
-   **Any docs page as Markdown**: Just append `.md` to any URL (e.g., `https://www.datocms.com/docs/content-management-api.md`)
    

Drop it into your favorite AI tool and see the difference. Build something with it. Break it. Tell us what you think.

Last week [Claude Skills](https://www.anthropic.com/news/skills) launched. Next week? Who knows what's coming. As AI tools evolve at breakneck speed, we'll keep making our documentation work better with them.

---

# Shopify vs. Headless CMS: A false binary

Source [blog]: https://www.datocms.com/blog/shopify-vs-headless-cms.md

Posted on [date: 2025-09-15T10:54:06.267+02:00] by Ronak Ganatra

## TLDR

-   Shopify is excellent at everything eCommerce - catalog, cart, checkout, payments, fulfillment...
-   Yes, it has a CMS, but that’s limited to products, a blog, static pages, and some metafields. It works for small stores, but could become restrictive quickly.
    
-   A headless CMS like Dato is designed for structured content, localization, editor workflows, and cross-platform delivery. But, it doesn’t do checkout, and that’s the point.
-   At a small scale, one tool can often be enough. For non-commerce sites, just a CMS. For simple shops, just Shopify. But as brands grow, especially across regions, with heavy content and campaigns, most end up needing both.
    

### False Binary

We often hear the same questions, just phrased a bit differently:

“*We’re comparing DatoCMS with Shopify, how do you stack up?*”

“*We’ve been happy with Dato, but we’re thinking about moving to Shopify*.”

“*We’ve outgrown Shopify, should we be moving to Dato?*”

It’s natural to frame it as an either/or choice. Both platforms store content. Both power websites. On the surface, it feels like an overlap. But in practice, they solve very different problems and come together quite well.

Shopify is an ecommerce engine with a CMSish feature set added. DatoCMS is a pure CMS. You can build with just one depending on your scope, but once projects grow past a certain size, you usually see both in the stack.

Case in point, peeps love the [Shopify plugin for picking products in DatoCMS](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-shopify-product.md).

Another case in point, we got tons of partners [building out dope projects using both](https://www.datocms.com/partners/showcase.md), but more on that later.

### Let's talk Shop-ify.

Legit, Shopify is among the best off-the-shelf eCommerce engines out there. It gives you product catalogs, variants, inventory, a checkout flow, payments, taxes, shipping, bells, whistles, and a massive app marketplace. That's before you even enter the rabbit hole of what you can do with Shopify Plus and Hydrogen.

Just thought of selling polka dot socks? You can get a working store online within an afternoon, which is why it powers shops for literally millions of small and mid-sized businesses.

If your site is essentially a “storefront + product descriptions + a blog post every now and then,” Shopify can handle it really well. For a small brand, Shopify often feels like the only tool you’ll ever need.

But.

Shopify also markets itself as having CMS features. Technically, that's true, but here’s what that means in practice:

-   You get products, collections, static pages, and one blog.
-   You *can* add extra data through metafields, but they still need to be defined by developers and the editing UI isn’t that intuitive.
    
-   The theme editor lets you drag and drop sections for a nice WYSIWYG feel, but it’s tied to your storefront theme and doesn’t scale well across multiple sites or campaigns once you level up your scale.
-   Content modeling is rigid. I mean this makes sense, they aren't a fully-fledged CMS with a schema builder.
    
-   Localization can feel clunky. Translations are tied to each individual field with no proper workflow for editors.
-   Content is storefront-bound. Repurposing it for apps, in-store screens, or other channels is possible, but paiiiinful.
    

For small-to-mid shops, this works great. For teams with huge eCommerce needs and bigger goals that extend beyond “product description plus add-to-cart,” not so much.

### Let's talk Headless

Ok, now when I say "Let's talk Headless", what I really mean is "Let's talk Dato", because, well, familiarity.

Anyways.

DatoCMS is a *proper* *headless* CMS. You can model any type of content: campaigns, recipes, editorials, testimonials, landing pages, product pages... And editors get a slick interface to manage it all.

[Localization](https://www.datocms.com/docs/general-concepts/localization.md) is built in. You can create workflows with fallbacks, side-by-side editing, and permissions that let teams work without stepping on each other.

The delivery is API-first, so you can send the same content to your site, your app, your email system, your dishwasher, my dishwasher, or to your in-store displays without duplicating any content or code. Developers get clean GraphQL and REST APIs. Editors get structured fields instead of free-text boxes.

However. What you *don’t* get out of the box is a checkout system. No carts, no payments, no fulfillment. Which is fine, because that’s exactly where Shopify excels.

That means if you're building a simple shop with a Headless CMS, you can, BUT you'll need something else like [react-use-cart](https://www.npmjs.com/package/react-use-cart) to handle your cart, and something like [Stripe](https://stripe.com/) to handle payments. OR go all in with something like a [commercetools](https://commercetools.com/) or [Commerce Layer](https://commercelayer.io/) to be your entire eCommerce engine.

But that also means, if you're building a bigger eCommerce project, you just tie up Dato with Shopify, and it's a win-win.

### When either isn't enough by itself

If you’re launching your first store, you sell a small set of products, and your marketing content is minimal, Shopify can carry you. You’ll get your storefront, checkout, and product catalog all in one place.

Think of a local coffee roaster. Their needs are product listings, a subscription app, and maybe a page explaining their sourcing. Shopify covers all of that without needing a CMS like us in the stack.

On the other hand, if you’re not selling products online, you don’t need Shopify. A SaaS site, a media brand, or a corporate site with multiple languages doesn’t need a checkout engine, they just need structured content.

DatoCMS also makes sense if your ecommerce is handled elsewhere. Some companies roll their own checkout flows with Stripe or use alternative commerce backends. In those cases, DatoCMS powers the content, and the payment layer is something entirely different.

Things get interesting once you scale. A growing DTC brand quickly runs into Shopify’s limits. They need campaign pages that aren’t tied to Liquid templates. They need regional sites with localized marketing content. They need editors who can launch seasonal campaigns without bothering the devs.

That’s where the Shopify + Dato pairing becomes really cool to work with. Shopify runs the transactional side like the catalog, the checkout, and the fulfillment. Dato manages the rest of the digital stuff like the homepage, campaign pages, localized content, editorial stuff, and distribution into multiple channels.

For devs, this means no more trying to bend Shopify’s page and blog system into a general CMS. For editors, it means working in a proper content interface instead of metafield spreadsheets.

But this sounds very preachy, so let's look at how we're seeing this combo work in the real world.

### What we're seeing

To prove the point, here are a few examples where Shopify and DatoCMS work side by side in production, showcasing different types of use-cases, from full-on-ecommerce-retail-brand, to not-ecommerce-brand-with-things-to-sell.

#### The Laundry Story by Cool&

(Image content)

Built as a headless Shopify Plus e-commerce platform, [The Laundry Story](https://www.datocms.com/partners/peter-coolen/showcase/the-laundry-story.md) offers a seamless and modern user experience. The website boasts stunning blog posts with cross-sell functionality, easy subscription options for your favorite products, and a modular page structure, thanks to [Cool&](https://www.datocms.com/partners/peter-coolen.md).

#### Root Houseplants by Attach Digital

(Image content)

[Root’s](https://www.datocms.com/partners/attach-digital/showcase/root-houseplants.md) older Wix site was slow with a fully-loaded time over 16 seconds causing issues for customers. As Root grew, moving into a new larger warehouse and opening their second physical store, they knew they needed a new website that could grow with them and offer more ecommerce functionality than Wix provided, so [Attach Digital](https://www.datocms.com/partners/attach-digital.md) hooked them up with a snappy Shopify+Dato combo.

#### Archie Rose Distilling by MindArc

(Image content)

[Archie Rose’s](https://www.datocms.com/partners/mindarc-agency/showcase/archie-rose-distilling-co.md) site uses Shopify Hydrogen for commerce for their [React](https://www.datocms.com/cms/react-cms.md) website with DatoCMS as the CMS. Hydrogen gives them a modern, performant storefront, while DatoCMS feeds in campaigns, product stories, and editorial content. The setup means developers from [MindArc](https://www.datocms.com/partners/mindarc-agency.md) aren’t locked into Shopify’s page builder, and editors can still manage content in a structured way.

#### Ricola by DEPT

(Image content)

[Ricola’s](https://www.datocms.com/partners/dept/showcase/ricola.md) global marketing platform, built by [DEPT](https://www.datocms.com/partners/dept.md), integrates Shopify directly into an [Astro](https://www.datocms.com/docs/astro.md) and DatoCMS-driven site. Dato handles localization and campaign content across multiple regions, while Shopify powers DTC sales. For a heritage brand with global reach, this pairing makes it possible to keep storytelling flexible while keeping ecommerce streamlined.

### A practical decision framework

So. Now that we've seen that the two aren't "interchangeable" as many think, the way to think about making a decision is simple.

Are you selling online? If yes, you need Shopify or some other commerce engine.

Are you publishing content that goes beyond product descriptions? If yes, you need a CMS.

Do you expect to run campaigns, manage localization, or deliver content into multiple channels? If yes, you’ll want Shopify and DatoCMS working side by side.

At small size, you can get away with one or the other. At scale, you almost always need both.

So the next time you’re wondering whether to choose one or the other, stop. That’s the wrong question. The right one is how far are you planning to scale, and [that's when we chat](https://www.datocms.com/contact.md) to see what's the best solution for your use case.

---

# What YOU 🫵 really want from your Headless CMS

Source [blog]: https://www.datocms.com/blog/what-you-really-want-from-your-headless-cms.md

Posted on [date: 2025-07-15T09:53:50.504+02:00] by Matteo Balocco and Ronak Ganatra

You know how we always flex about us not having ALLLLL the features under the sun but "[just enough](https://www.datocms.com/features.md)"? Well this is why. Because CLEARLY we don't want to build some complex, bloated, unused, buzzword-ridden pile of features that you don't care about (I mean, we got plugins for that innit?). And even more clearly, you wouldn't want to use that.

How do we know? We spoke to you 💁‍♀️

Similar to the [extensive research we did in '23](https://www.datocms.com/blog/connecting-with-the-datocms-community-unpacking-our-customer-research.md), I sat down with many of you again this year to unpack how you're using DatoCMS, what you like, what you don't, what you're missing, and what we can work on to improve things for you.

So while we unpack all that research into our own internal planning, check out what we've been discussing to get an idea of how your peers are using the CMS and what could be cool to explore together.

Buckle up - this one's gonna be a long read.

---

## Headless is boring (that's good!)

Every single person we interviewed has moved to headless (in retrospect, I mean, this one was fairly obvious 😅).

No one’s looking back. The decision to decouple content from presentation is now muscle memory. Monoliths just aren’t in the conversation anymore unless there’s a very specific legacy use case.

But why is it "boring"? What used to be a bold architectural move is now just… expected. Headless isn't hype, it's not "cool", it's not revolutionary, it's the norm.

> Once we moved to headless, we never looked back. The boundaries are clearer, the flexibility’s better, it just makes sense now.

API-first? Of course. Structured content? Naturally. Integration-friendly? Bare minimum.

So if everyone’s already headless, what are people actually evaluating? Turns out: DX, UX, and how well the CMS fits into their *real* workflows.

## CMS isn't the *first* decision

Multiple teams told us they’re delaying the CMS decision until a project is more defined. Early stages are often built without one at all, just some mock data, maybe a Notion doc, or even AI-generated content. Once it’s clear a project needs a proper content hub, then a CMS enters the picture for a real evaluation.

This shift is important. It means people don’t default to a CMS anymore. They wait to see if it’s worth bringing one in.

> We often skip the CMS entirely in early phases: mock data or docs does the job until the project’s real enough to justify one.

And when they do, the bar’s higher than it used to be. Just having an API or supporting tomorrow's cool JS framework isn't enough. If it doesn’t fit into your setup cleanly, or forces too much custom logic to make it behave, it’s out. The market's seen enough CMS saturation and maturity for clear choices to be made with informed decisions, rather than just choosing any Headless CMS for any project.

## DX still wins (and loses) deals

Developer experience isn’t just a nice-to-have anymore, it’s often the deciding factor.

When teams are choosing a CMS, they’re not just asking “what can this thing here do,” they’re asking “how much of a pain in the a\*\* is it going to be to maintain?” And that includes everything from how fast the APIs respond, to how easily it fits into their stack, to whether they can debug an issue without digging through obfuscated UIs.

Performance matters. Not just site speed and API speeds, but interface speed.

> If the media library takes forever to load, the client’s already annoyed, and now *you* look bad. So do I.

A good DX means devs can move quickly, trust the platform, and hand it off to non-technical teammates without dreading the support questions that’ll follow.

A bad DX? That’s how migrations start.

And migraines.

> I can love working in the CMS, but if it makes life harder for the next person in line, then it’s not working.

## Editors just want things to work

And here’s the flip side. Editors don’t care about GraphQL or schema migrations, most of the time they barely know what's under the hood (why should they). They care that the preview works. That the text editor doesn’t screw things up in formatting. That they can upload a 300000000GB (*please don't*) video and expect it to come on the frontend optimized without asking the dev team what’s wrong.

A bunch of teams mentioned building small tools on top of the CMS to patch things, whether it’s nicer image previewing, easier entry duplication, or filtering that works.

> Good UX beats clever features every time. Our clients don’t care what’s under the hood as long as it’s smooth.

We take that as a compliment, weirdly, because we want you to build plugins that seamlessly enable your workflows rather than focusing on 50 shades of features and then making you tweak your flows to fit ours. But it also shows there’s room to streamline some of these flows if enough of you think it's a core requirement.

## No/Low-Code isn't evil, but it's also not a ✨magic✨ fix

Nearly everyone had some opinions on no-code and visual builder tools.

The pattern was consistent: healthy skepticism from devs, initial excitement from clients, and then… reality.

One dev said straight up that the code output from the visual builder they use is “garbage.” Another mentioned they still use \*\*\*\*\*\* but only by injecting “a ton of custom code” to make it work the way they want.

It’s not that no-code is useless. It’s just that the moment a project grows beyond basic layouts and static content, people start hitting their head on walls.

> Clients love it at first. Then they realise it’s not a magic tool.

For high-stakes or high-quality builds, teams always come back to developer-led workflows. No-code has its place: MVPs, small sites, client previews, but it’s not replacing structured content models and frontend frameworks anytime soon.

Not because of dogma. Just because it breaks down under pressure.

## AI is creeping *everywhere*

Bet you were surprised it took this long to get to AI huh? This year, AI came up even when we didn’t ask about it.

Devs are using it for code completion, repetitive query building, or generating test content. Editors are starting to expect it for things like alt text generation, summarizing articles, SEO boosts, and translations.

It's creeping into almost every workflow.

But the most interesting bit was this: nobody wants AI to replace real people. They just want it to get rid of the boring bits.

> We’ve started building our own AI tools because the built-in stuff in many CMS is either too generic or too limited.

There’s also a clear pattern of people building their own AI workflows rather than using whatever generic tool is baked into the CMS. The request isn’t “add AI to Dato”, it’s “make it easy to hook in the AI we already use.”

Which is music to our ears, because we've avoided just "sprinkling" AI into the CMS to fulfil a hype for SEO. We're definitely thinking about how and when to include more AI, but aside from our [gorgeous and business-critical emoji picker](https://www.datocms.com/product-updates/emoji-picker-for-models-and-blocks.md), we don't want to just slap on a generative content field and call it a day, so stay tuned for where this conversation will go.

In the meanwhile, we already got SOME plugins that scratch that AI itch:

-   [Alt Text AI](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-alt-text-ai.md)
-   [AI Translations](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md), and
    
-   [AI Content Generator](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-chat-gpt-ai-content-generator.md)
    

There's a [few more if you look around the marketplace](https://www.datocms.com/marketplace/plugins/browse.md?s=AI).

Oh and speaking of plugins...

## Plugins aren't just "nice to have"

Several of you told us that DatoCMS plugins were the reason they didn’t migrate away. Not because they use *all* of them. But because they could build or install exactly what they needed without waiting on us to ship it.

That's the benefit of hosting your own super specific plugins - the public/community ones are just the tip of the iceberg when it comes to maxxing out Dato's capabilities. Some of you have really WILD private plugins making the CMS do all sorts of things 🤯

Modularity is a double-edged sword though. Some folks are worried about relying too much on community third-party plugins that might disappear or break. A few of you called this “long-term liability.”

So your vibe here seems to be to give you the flexibility, but don’t make us depend on it. Valid.

## Enterprise features are *also* important to non-enterprise

Even dev-heavy teams are starting to care about “enterprise” stuff: audit logs, role management, permissions, data residency (shoutout to Switzerland for popping up multiple times), and predictable pricing.

No one’s "excited" when talking about these things, but everyone wants them, especially agencies managing multiple client projects at scale.

More than one person said they lost time or trust because another platform changed its pricing model overnight or killed off a feature without warning.

What used to be “nice to have for bigger companies” is now just... expected as well.

So we're definitely looking into how we can make some of these topics slightly more "normalized" for all the users.

## CMS expectations have changed

The biggest shift from the '23 chat to now is that people no longer see the CMS as the “main thing.”

It’s just one part of the stack. Sometimes late to the party. Always expected to integrate cleanly and never be the bottleneck.

You can call that boring if you want, but as we said way up, we think it’s a sign of maturity.

In the end, your CMS shouldn't be the tool you talk about every week (really fun topic at parties eh?). It should be the one that gets out of the way and lets you focus on everything else.

---

## So what's next?

This research isn’t just for a blog post. It’s what we use to figure out what to build, and more importantly, what not to.

We’re already thinking/talking about:

-   Better schema-as-code workflows
-   Smarter ways to support AI integration (your AI, not ours, for the moment)
    
-   More predictable pricing structures to avoid surprises
-   Improvements to media handling
    
-   And generally making the platform feel snappier and more flexible across the board
    

We’ll share more as we build. For now, if something here resonated, or if you’re rolling your eyes because *your* pain point didn’t make the list, send me a ping.

[Let's chat!](https://forms.datocms.com/form/W_5PwC3kQy6585D09e4lUA)

☝️ Seriously. This stuff only works if it’s a two-way conversation, and I'm always down to talk!

---

# Working with Schema Import/Exports and Recipes

Source [blog]: https://www.datocms.com/blog/schema-import-exports-and-recipes.md

Posted on [date: 2025-07-14T10:53:45.311+02:00] by Ronak Ganatra

### TLDR

[Recipes](https://www.datocms.com/marketplace/recipes.md) and the [Import/Export plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-schema-import-export.md) in DatoCMS helps you avoid repetitive setup, standardize models across projects, and onboard faster. Like way faster. They reduce manual work and help scale content architecture across teams and environments.

#### What you can do with this combo:

-   Onboard new devs with actual working schema instead of docs and a blank project
-   Spin up new projects or micro-sites using fully-fledged models and blocks you've created before
    
-   Keep structure consistent across projects, teams, and brands
-   Extend existing schema elements into new ones with slight tweaks
    
-   Recover deleted or deprecated models without starting over (⚠️ provided you keep backups, don't @ me)
-   Build frontend component libraries backed by reusable schema
    

If you've ever worked across more than one DatoCMS project, this setup probably saves you hours.

### But first. What're we talking about?

ICYMI, we recently rolled out the [Schema Import and Export plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-schema-import-export.md) not too long ago, which lets you export one or multiple models/block models as JSON files for reimport into another DatoCMS project. WITH all relevant relations, if you so choose.

This means, you can:

-   Export single or multiple models/block models as JSON files
-   Import models into different DatoCMS projects
    
-   Smart conflict resolution with guided instructions
-   Automatic plugin dependency detection and inclusion
    
-   Safe import operations that preserve existing schema
    

This unlocks really fun use cases by itself, but having this plugin installed ALSO let's you easily import some readymade models and blocks to kickstart common projects that need things like a blog post or a newsletter signup.

Which is why we doubled down on schema imports by introducing [Schema Recipes](https://www.datocms.com/marketplace/recipes.md) - a curated selection of ready-to-use models and blocks that you can import into your projects.

The first wave has common marketing blocks and models for:

-   [Global settings](https://www.datocms.com/marketplace/recipes/global-settings.md) for things like navs and branding
-   [Blog post](https://www.datocms.com/marketplace/recipes/blog-post.md) with authors
    
-   Simple [marketing landing pages](https://www.datocms.com/marketplace/recipes/marketing-landing-page.md) built with blocks
-   [Case studies](https://www.datocms.com/marketplace/recipes/case-study.md) to flex your wins
    
-   A simple [newsletter signup](https://www.datocms.com/marketplace/recipes/newsletter-signup.md) connected to your external service, and
-   A simple (and annoying, depending on how you look at it) [marketing pop-up](https://www.datocms.com/marketplace/recipes/marketing-exit-intent.md)
    

Just check any of them out, hit "import" and we'll move it into whichever project you choose.

Nifty, eh?

Anyways...

For added context, when we say "recipes" it can either mean the community ones available via the marketplace, OR whatever schema you export and save as a JSON file to be reused across projects.

And also, the real magic kicks in when you pair these schema exports with frontend component libraries. Say your dev team builds a reusable `HeroBanner` component. You can keep the corresponding JSON schema export in the same repo. Anyone starting a new project can pull both in, the schema and the frontend, and have it running in minutes. 🤯

That’s what makes it work long-term. It’s not just about spinning up new DatoCMS models, it’s about standardizing content + presentation in a predictable, maintainable way. it can either mean the community ones available via the marketplace, OR whatever schema you export and save as a JSON file to be reused across projects.

OK, moving on.

### Why structure matters

When you manage more than one DatoCMS project, or even just one really large one, the last thing you want to do is manually recreate models by hand. Why waste hours setting up the same blog, the same page builder, or the same SEO meta fields across projects?

Recipes and the Schema Import/Export plugin solve this. Recipes let you define reusable configurations for models and fields. Schema Import/Export gives you safe, conflict-aware migrations of actual model definitions between projects, with all dependencies handled.

### Looking at how this works in practice

#### Onboarding new devs

If you're onboarding a new dev to your project, instead of asking them to poke around a production schema and reverse-engineer your work, you could just:

-   Start a fresh DatoCMS project
-   Import the blog, hero, and modular page builder models using JSON exports from your main project
    
-   Drop in a few recipes to add global settings, favicon fields, and meta tag configs
-   Let them play around
    

Now they have something they can actually build and test against.

Want them to understand how a multi-block landing page works? It’s already there.

Need to build previews or test image components? No need to fake fields or guess validation logic, it’s all imported.

(Image content)

Much joy

They get real models, with real structure, ready to plug into your existing codebase if needed.

#### Spinning up new projects

You’ve got a marketing website, a campaign page, or a new market going live. You don’t want to redo the same structure again because you've spent way too long perfecting one project with all the presentation options and validations.

-   Create recipes to scaffold common structures (landing pages, marketing pop-ups, reusable CTAs)
-   Use the Import/Export plugin to bring in specific shared components from another project (e.g. FAQs, testimonials, pricing blocks)
    
-   Run a migration script to bring in all the relevant content and assets into the new project structure without breaking a sweat.
    

Sounds vague? Let's imagine your team is launching a new product or site under the same umbrella brand.

You run multiple DatoCMS projects, maybe for different brands, maybe for different regions, and every team keeps naming the same thing slightly differently. That’s a problem. Especially when your frontend expects specific field keys or block structures.

With schema exports, you define your blocks once, like `CtaBlock`, `NewsletterSignup`, or `PromoBanner`, export them, and distribute the JSON to other teams or environments. The plugin safely merges the schema into each project. No accidental overrides. No duplicate field errors. Just consistent blocks, ready to go.

(Image content)

You don’t need to create the hero block from scratch or guess which field type the image uses. Just export it from the last project, import it here, and move on.

(Video content)

In fact - we recently collab'd with the fine folks at Bejamas to explore this exact use case. They built a gorgeous new Astro theme called [Multilaunch to handle multiple projects for different brands](https://www.datocms.com/casual-chats/on-building-astro-themes-for-multi-brand-websites.md) under the same corporate structure, something that would be a breeze to manage with the plugin. [Check out everything that covers](https://www.datocms.com/casual-chats/on-building-astro-themes-for-multi-brand-websites.md)!

If you’ve got localized versions to roll out (where just introducing i18n into your parent project doesn't make sense), it’s even better: same models, same logic, just localised content. The structure is already battle-tested.

#### Extending existing schema

Who says you always gotta be spinning up a new project for making the most of the plugin?

Let's say you got a really big and complicated website, and there's a very specific case study model with 50+ fields to showcase all the incredible work you did with some customers.

But now marketing wants to recreate this for showcasing work done with partners too, just that they'd need a few tweaks to the fields and 1-2 changes.

You really gonna sit there and make the whole model from scratch? Again? Nah fam.

(Image content)

Instead of rebuilding, you export the existing model, import it into your project, rename it to `PartnerCase`, tweak the field labels or validations, and publish. Done.

You keep consistency where it matters, and customize only what you need. Much faster than duplicating models by hand.

#### Recovering and restoring projects

Say someone deleted a model. Or you sunset a block last month that marketing suddenly wants to use again, but you don’t want to guess how it was set up.

(Image content)

If you’ve got a JSON export of that model from periodic backups, you’re covered (general life best practices to retain some things, just to be safe). Drop it into the Import/Export plugin, review the schema summary, and it’ll bring the block back exactly as it was: same fields, same validations, same plugin dependencies, same appearances. They'd never even know it was gone.

This is especially useful for blocks that were used across multiple pages or components, things that take time to reconstruct. You don’t need to dig through version history or re-read changelogs. Just re-import the schema and continue using it immediately.

Know what's even better? The plugin will tell you if anything in the incoming schema overlaps with your current project setup. So if you’re reviving an older block in a project that’s evolved since, you’ll see what might conflict before you confirm anything.

If you’re smart about keeping exports of your schemas (e.g. in your repo or backup folder), this becomes a lightweight version of schema version control. You’re not reinventing anything. You’re just rolling forward with the old work you already vetted.

---

Using the plugin is really a super-safe way to scale your schema. This plugin won’t overwrite anything. It only adds new models or blocks and shows you conflicts before anything is applied. It pulls in any dependencies automatically, related blocks, required plugins, field appearance settings, and validations, without you having to double-check everything manually.

It’s schema portability with guardrails. You keep control. And your team doesn’t waste hours manually replicating things project to project.

And if you’re just starting out, the Recipes are a solid base layer. Import them, tweak what you need, and move on to the actual build.

Future you will thank current you.

---

# Dato Turns 10 or: How to stay alive eating like the Italians

Source [blog]: https://www.datocms.com/blog/dato-turns-ten.md

Posted on [date: 2025-07-02T09:24:11.150+02:00] by Stefano Verna

This past May marked a pretty special milestone for us — ten years since the first lines of code that would become DatoCMS were committed!

(Image content)

A decade later, still struggling with writing commit messages

Sure, the company itself isn't quite ten yet — as you can see from my git email at the time — but we weren't about to let that technicality stop us from throwing a little celebration! More importantly, it gave us the perfect reason to do something we'd never done before: get our entire remote team in the same room.

We're a fully remote company. Many of us have only seen each other on the occasional calls and Basecamp threads. Some of us have worked here for years and had never shared a meal in person. So for the 10-year mark, we decided to do something simple: meet, eat, and hang out. Somewhere sunny.

Since we're mostly Italian anyway (save for 2 token non-Italians 🤌), we chose to melt and stuff our faces in the Tuscan countryside.

We also had a no-work no-laptop thing going on, so if you're salty with Roger or Marcelo for delayed support times, I'm the one to blame 🫥

Some of us came from Turin, a couple from around Tuscany, and our token non-Italians from the US and Berlin. It was great to finally see the 3 Matteos together — for all of you who've been as confused as us when you enter a call and hear "Matteo," the math is honestly ridiculous: 13 total team members, 3 of them named Matteo, which gives us a solid 25% Matteo concentration rate.

(Image content)

The lethal trifecta©

There were handshakes and hugs, but it didn't take long for the food to come out. We kicked things off with our [first lunch together](https://www.ristorodilamole.it/). And since we're in Italy, that meant the full service antipasti \> primi \> secondi \> dolce \> caffè \> ammazzacaffè stretch:

(Image content)

Watch us, happy and completely unaware of the binge that would follow

To wash off the food coma (which would pop up at least twice a day), we loaded into cars and drove into the Tuscan hills — classic rolling-green-wine-country stuff — to a villa where we could shake off the meat sweats by the pool.

(Image content)

How long was dinner? Slow and heavy, as all good dinners should be. What did we do after? Rallied just enough to play a few rounds of *Time's Up* — you know, the charades game where everyone gets increasingly dramatic.

The next day came with a sliver of activity which is the most mobile we'd be all week (after breakfast, of course), a super fun quad-bike session across the Tuscan hills, through vineyards, up to medieval towns, and down rocky hills.

(Image content)

Of course, ending with [another packed lunch](https://isaliciagriturismo.com/). After which came dinner. But this one hit different. We brought out the champagne and cake to celebrate the milestone we've all achieved together thanks to you.

(Image content)

Millefoglie all around! You reading this right now definitely deserve a slice

The scorecard? Still here, still small, still independent, still profitable, still doing things our way with no plans to change that.

No speeches, just championing through a slice while we were all fainting from the lasagne earlier.

And finally, to wrap things up the next day, the final day. What came first? Breakfast, of course. What came next? One last road trip to visit a [legendary Tuscan butcher](https://www.dariocecchini.com/) an hour away for what can only be described as a four-hour steak marathon.

The damage report: eight courses, each one better than the last, each one stacking up the meat sweats.

(Image content)

And then we were done. There's not much of a moral here. No big takeaway. So what was the point of this post without adding any typical 10-year timeline or life lesson of what's to come and what aspect of content management we're looking to revolutionize with AI?

Nothing really — just taking a few moments to distract ourselves from the daily work we do to spend time getting to know the people behind the avatars, and share the people behind the CMS with you 👇

(Image content)

Ten years is a long time in SaaS. Long enough to have survived wave after wave of hyped frontend frameworks, multiple generations of CMS trends, and then some. The product's changed a lot. The team's changed a bit. The vibe? Still the same — build something good, don't overthink it, have a laugh.

(Image content)

So I'm not pushing some oracle vision of "here's the next 10 years of Dato", but now that we've digested away our gluttony, and appreciated all we've managed to accomplish together, we can get back to focusing on you and continuing to build on the CMS since you are the reason we do what we do.

(Image content)

👋🧡

---

# Building MultiLaunch (Part 2/3): The CMS Experience for Editors

Source [blog]: https://www.datocms.com/blog/building-multilaunch-part-2.md

Posted on [date: 2025-05-06T09:36:01.347+02:00] by Ronak Ganatra and Mojtaba Seyedi

## TL:DR

This is part 2 of a 3 part series on [MultiLaunch](https://astro-dato-multilaunch.vercel.app/) - an Astro Theme built by Bejamas using DatoCMS, Astro, and Vercel.

-   [Part 1 covers the setup of the CMS](https://www.datocms.com/blog/building-multilaunch-part-1.md), the content modeling, and the schema.
-   [Part 2 talks about the overall editorial experience](https://www.datocms.com/blog/building-multilaunch-part-2.md) using the project and the use of plugins.
    
-   [Part 3 covers the monorepo and the overall DX](https://www.datocms.com/blog/building-multilaunch-part-3.md) and deployment when working with this theme.
    

If you're looking for quick links to take it all for a spin, here's what you need 👇

-   Check out the demo here: [https://astro-dato-multilaunch.vercel.app/](https://astro-dato-multilaunch.vercel.app/de)
-   Fork the repo here: [https://github.com/bejamas/astro-dato-multilaunch](https://github.com/bejamas/astro-dato-multilaunch)
    
-   Check out the details on the official Astro theme library here: [https://astro.build/themes/details/multilaunch-multi-brand-website-template/](https://astro.build/themes/details/multilaunch-multi-brand-website-template/)
    

We also had a [really nice chat with Mojtaba](https://www.datocms.com/casual-chats/on-building-astro-themes-for-multi-brand-websites.md) on his approach to the whole project, so check that out too!

---

With the way the CMS is set up for MultiLaunch, editors can launch a new website, pick a brand color, translate content into five languages, and go live, without touching any code. In this post, we’ll show how MultiLaunch makes that possible inside DatoCMS.

The reason why talking about the editorial experience by itself matters, is because the goal with MultiLaunch wasn’t just to make things easier for devs. It was to make launching and maintaining content across multiple brand sites a zero-stress experience for editors, too, considering that this theme was very much built from real world experiences of working with clients at that scale.

No dragging content into place. No fighting with design tools. No guessing what a field does. Just clear, structured inputs and immediate results.

This is the DatoCMS editorial layer in MultiLaunch. It’s surprisingly clean, and most of that power comes from keeping things simple and straightforward.

### Working off an opinionated schema

Every brand in MultiLaunch runs off the same schema. That schema is opinionated, and that’s a good thing.

(Video content)

Editors don’t get a blank canvas. They get clear content types:

-   Homepage (made from modular blocks)
-   Brand info (logos, features, references, colors...)
    
-   Theme (fonts, max width, dark/light mode settings)
-   Layout (shared navigation, footer, etc.)
    

When editors add or update content, they’re not touching layout. They’re just filling in content for the system to render.

> We don’t let editors define layout. They define content. That keeps everything clean.

That separation between structure and presentation is what makes MultiLaunch so easy to scale.

### Flexible and Modular editing

The homepage of each brand site is built using modular blocks. These blocks are reusable and rearrangeable. Think CTAs, content sections, reviews, feature lists.

If an editor wants to change the order of sections on the homepage, they can. If they want to add a new review or a second CTA, that’s fine too.

(Video content)

But they’re not creating new layouts or writing any markup. They’re just stacking existing components within certain guardrails. Each block is pre-designed, pre-tested, and tied to a schema.

This way, the frontend always looks right, and the editor can never “break” the design.

To give editors even more confidence while working in the CMS, MultiLaunch includes a few visual enhancements.

(Video content)

The Visual Select plugin is used in content types like the contact form. Editors can choose between layout options (like vertical or horizontal forms) by selecting visual previews instead of guessing what “layout-1” or “layout-2” means.

And for more dynamic views, there’s also Web Previews, giving editors a live visual of what they’re changing.

(Video content)

This preview system mirrors the actual frontend. So instead of previewing a generic page, editors see exactly how their update will appear on the live site, fonts, colors, layout, everything.

On the localization side, each brand site in MultiLaunch supports multiple locales, five "out of the box" with English, French, German, Dutch, and Polish.

DatoCMS handles localization with tabs across fields. Editors can toggle between locales and add translations for any piece of content.

But instead of doing that manually, MultiLaunch also ships with the AI Translator plugin.

(Video content)

Here’s how it works: you write your content in English, click “Translate to all locales,” and DatoCMS generates translations across every field, product descriptions, reviews, CTAs, the whole thing.

> I wrote the English content, clicked translate, and it just happened. That part honestly blew my mind.

The translations aren’t perfect, but they’re more than good enough to use as a baseline, and for teams without dedicated localization resources, this feature saves hours.

There’s also a review section powered by the Star Rating plugin. Editors can click to set ratings instead of typing numbers. Small touch, but nice UX.

Again, all of this is scoped to content and brand identity. No layout logic. No conditional zones. No DIY page building.

### How content should feel

MultiLaunch works because it doesn’t pretend editors want to build the site. It assumes they want to manage content.

They want to update a description, add a new brand, tweak the homepage flow, or launch in a new market, and they want to do that without asking for dev time or touching code.

DatoCMS makes that possible here, and the editorial layer in MultiLaunch shows what good schema design, minimal plugin use, and thoughtful modeling can do.

From the editor's point of view, DatoCMS looks and feels clean. There are no unused fields, no optional overrides, no surprises.

The UI is fast. The localization tabs are intuitive. The field labels are written in plain language.

And because the schema is shared across all brands, training is basically one-and-done. Once someone knows how to edit Brand A, they can edit Brands B through Z with zero confusion.

Even onboarding someone new is quick. You don't teach them how to build pages, you show them where to edit structured content.

---

In the next and final post of this series, we’ll switch back perspectives to the devs to talk about the monorepo architecture, how slugs control brand deployments, why Astro was chosen, and how the entire setup was designed to be fast and easy to scale.

---

# Building MultiLaunch (Part 3/3): The Repo and the DX

Source [blog]: https://www.datocms.com/blog/building-multilaunch-part-3.md

Posted on [date: 2025-05-06T09:37:05.000+02:00] by Ronak Ganatra and Mojtaba Seyedi

## TL:DR

This is part 3 of a 3 part series on [MultiLaunch](https://astro-dato-multilaunch.vercel.app/) - an Astro Theme built by Bejamas using DatoCMS, Astro, and Vercel.

-   [Part 1 covers the setup of the CMS](https://www.datocms.com/blog/building-multilaunch-part-1.md), the content modeling, and the schema.
-   [Part 2 talks about the overall editorial experience](https://www.datocms.com/blog/building-multilaunch-part-2.md) using the project and the use of plugins.
    
-   [Part 3 covers the monorepo and the overall DX](https://www.datocms.com/blog/building-multilaunch-part-3.md) and deployment when working with this theme.
    

If you're looking for quick links to take it all for a spin, here's what you need 👇

-   Check out the demo here: [https://astro-dato-multilaunch.vercel.app/](https://astro-dato-multilaunch.vercel.app/de)
-   Fork the repo here: [https://github.com/bejamas/astro-dato-multilaunch](https://github.com/bejamas/astro-dato-multilaunch)
    
-   Check out the details on the official Astro theme library here: [https://astro.build/themes/details/multilaunch-multi-brand-website-template/](https://astro.build/themes/details/multilaunch-multi-brand-website-template/)
    

We also had a [really nice chat with Mojtaba](https://www.datocms.com/casual-chats/on-building-astro-themes-for-multi-brand-websites.md) on his approach to the whole project, so check that out too!

---

Ok let's get riiiiight into it.

### Looking at the repo

MultiLaunch was designed for companies running multiple brand sites. The dev experience needed to match that goal, which is why its built as a monorepo with [Astro](https://www.datocms.com/docs/astro.md), DatoCMS, and [Vercel](https://www.datocms.com/marketplace/hosting/vercel.md). One repo. One CMS. As many brand sites as you need without duplicating code, content models, or deployments.

(Image content)

It had to be DRY. It had to be scalable. It had to let you launch new brands without spinning up new projects or writing new routes.

Which is why, from the start, the answer for Mojtaba was simple: build it as a system, not a one-off template. That meant a monorepo setup, with a shared UI layer, config logic based on slugs, and a CMS that could feed all of it.

At the top level, the repo has:

-   `apps/core` — for the main site
-   `apps/brand` — for individual brand sites
    
-   `packages/ui` — for shared components and design tokens
    

Each app is its own Astro instance, but they pull from shared logic and styles in `packages/ui`. So if you update a button style or fix a bug in a component, that fix rolls out everywhere.

This also means there's no duplication when building new sites. Everything from layouts to typography to SEO logic is shared, unless you explicitly override it.

(Image content)

On the individual brand side of things, each brand deployment is controlled by a `BRAND_SLUG` env variable. That slug maps directly to a brand entry in DatoCMS.

Here’s how it works:

1.  Create a new brand record in DatoCMS
    
2.  Create a new Vercel project pointing to `apps/brand`
    
3.  Add `BRAND_SLUG=some-brand` in the env vars
    
4.  Hit deploy
    

And just like that, you've got a brand new shiny website up and running for a new brand, complete with consistent branding and structure to match the others.

New site on a custom domain, powered by the same frontend, same schema, same logic. Easy.

> You don’t even have to touch your code. You just deploy a new project with a different slug.

When working locally in dev, you do the same thing with `.env` files. That means no branching. No boilerplate copying. No guessing where things live.

(Video content)

### But why Astro?

Now, we're big fans of Astro here at DatoCMS, so this wasn't just a "We love it let's do it with this" kinda story. There's very well-founded reasons for the Bejamas crew opting for Astro.

First. Performance. Astro ships zero JavaScript by default. For mostly-[static brand sites, this is a huge win](https://www.datocms.com/blog/comparing-js-frameworks-for-content-heavy-sites.md). Pages load fast, Core Web Vitals are great out of the box, and you don’t need to fine-tune hydration.

Second. Flexibility. Astro works with React, Vue, Svelte, Solid... (I mean islands are just dope). You can bring in what you need, where you need it.

Third. File-based routing and localization are straightforward. With minimal setup, each locale can be its own route. And since everything is based on slugs, the routing logic stays clean.

MultiLaunch also uses the official [DatoCMS CDA client](https://github.com/datocms/cda-client/tree/main) ([`@datocms/cda-client`](https://github.com/datocms/cda-client/tree/main)) to query content. It’s wrapped in a utility function that handles slugs and locales, so components don’t need to deal with query logic directly.

You pass in the brand slug, the locale, and the query, and get back clean, typed data.

This keeps the Astro pages lean and focused on rendering.

### Let's talk under the hood

Here's a quick look into the considerations that really contribute to the overall DX when working with this theme.

But first, a quick look into how the repo is structured.

(Video content)

#### Build Triggers for smoooooth CI/CD

Once a brand site is live, publishing new content is completely decoupled from the dev team. DatoCMS has [native build trigger support](https://www.datocms.com/marketplace/hosting/vercel.md), which is hooked up to Vercel deployments.

That means any content update can trigger a rebuild and redeploy, no code pushes needed.

You could be running 5, 10, 50, 100 brand sites, and none of them require individual attention after setup.

#### Bun over NPM

Also, this project now runs with [Bun](https://bun.sh/). It’s faster for installs and plays well with Vercel. And they got a hella cute logo.

> I switched to Bun after running into an install issue on Vercel. It just worked. It was faster too, so I stuck with it.

That’s one of those small DX quality-of-life things that adds up when you’re managing many deployments.

#### UI Packaging

All UI components live in `packages/ui`. This includes typography, layout sections, buttons, review cards, and form components.

(Image content)

That package is used across both `core` and `brand` apps. So if the design system evolves, you’re not updating components in multiple places.

And because everything is schema-driven, the components are flexible. They accept props passed from DatoCMS queries like brand colors, section order, content blocks, and render them accordingly.

#### Built-in SEO

Each page in MultiLaunch pulls its own metadata from DatoCMS. The CDA client queries include fields like `metaTitle`, `metaDescription`, `canonical`, and `ogImage`.

```graphql
_seoMetaTags {
        tag
        attributes
        content
      }
```

Those get passed to a layout-level SEO component, which uses the official DatoCMS `<StructuredMeta>` component from `@datocms/astro`.

It’s easy to implement and ensures that every brand site is search-ready out of the box.

#### Geo-redirects

As a last flex, The system also supports geo-based redirects.

There’s middleware in Astro that detects a user’s country and sends them to the right localized version of the site. For example, a visitor from Germany gets redirected to `/de`.

 ```typescript
 // Get user's country from Vercel geo data
  const country = request.headers.get('x-vercel-ip-country')?.toLowerCase() || ''

  // Map countries to locales (extend this mapping as needed)
  let locale = DEFAULT_LOCALE
  if (['fr', 'be', 'ch'].includes(country)) {
    locale = 'fr'
  } else if (['de', 'at', 'ch'].includes(country)) {
    locale = 'de'
  } else if (['pl'].includes(country)) {
    locale = 'pl'
  } else if (['cn', 'hk', 'tw'].includes(country)) {
    locale = 'zh'
  } else if (['sa', 'ae', 'eg', 'iq', 'jo', 'kw', 'lb', 'om', 'qa', 'sy'].includes(country)) {
    locale = 'ar'
  }

  // Redirect to the appropriate locale
  return new Response(null, {
    status: 302,
    headers: {
      Location: `/${locale}${pathname === '/' ? '' : pathname}${url.search}`
    }
  })
}
```

It’s a small touch, but it makes the whole experience feel polished, especially when you’re managing brands across multiple countries.

---

To wrap things up, this setup works because it's consistent.

-   One repo
-   One CMS
    
-   One schema
-   One query pattern
    
-   One UI system
    

You don’t reinvent anything when launching a new site. You just plug in new data. Everything else flows from that.

> It’s fast to work with, easy to extend, and super clean to manage.

And that’s the point. MultiLaunch was built to scale, not just content-wise, but technically. It’s designed so you don’t have to rebuild the same logic every time.

Bonus point? Performance for the end-user. Because no new frontend project is ever really complete without flexing those [token Lighthouse scores](https://pagespeed.web.dev/analysis/https-astro-dato-multilaunch-vercel-app-en/b1m08dnm8i?form_factor=desktop) 💅

(Image content)

[So take it for a spin](https://astro.build/themes/details/multilaunch-multi-brand-website-template/), and let us know what you're building with it!

---

# Building MultiLaunch (Part 1/3): Structuring DatoCMS Like a System

Source [blog]: https://www.datocms.com/blog/building-multilaunch-part-1.md

Posted on [date: 2025-05-06T09:35:52.235+02:00] by Ronak Ganatra and Mojtaba Seyedi

## TL:DR

This is part 1 of a 3 part series on [MultiLaunch](https://astro-dato-multilaunch.vercel.app/) - an Astro Theme built by Bejamas using DatoCMS, Astro, and Vercel.

-   [Part 1 covers the setup of the CMS](https://www.datocms.com/blog/building-multilaunch-part-1.md), the content modeling, and the schema.
-   [Part 2 talks about the overall editorial experience](https://www.datocms.com/blog/building-multilaunch-part-2.md) using the project and the use of plugins.
    
-   [Part 3 covers the monorepo and the overall DX](https://www.datocms.com/blog/building-multilaunch-part-3.md) and deployment when working with this theme.
    

If you're looking for quick links to take it all for a spin, here's what you need 👇

-   Check out the demo here: [https://astro-dato-multilaunch.vercel.app/](https://astro-dato-multilaunch.vercel.app/de)
-   Fork the repo here: [https://github.com/bejamas/astro-dato-multilaunch](https://github.com/bejamas/astro-dato-multilaunch)
    
-   Check out the details on the official Astro theme library here: [https://astro.build/themes/details/multilaunch-multi-brand-website-template/](https://astro.build/themes/details/multilaunch-multi-brand-website-template/)
    

We also had a [really nice chat with Mojtaba](https://www.datocms.com/casual-chats/on-building-astro-themes-for-multi-brand-websites.md) on his approach to the whole project, so check that out too!

### Why build this?

(Video content)

Ok, so why does the world need another cloneable template to spin up a new project?

Managing multiple brand or market websites usually sucks.

You either duplicate repos and tweak each one manually, or you try to over-generalize everything into a brittle “one-size-fits-all” site that nobody’s happy with. Content gets out of sync. Code gets messy. Launches slow down. Every new site feels like a new projectnot a new instance.

MultiLaunch was built to fix that.

It isn't *just* another OSS eCom template to spin up a simple shop. In fact, there's no eCommerce integrations at all. It's purely for content management from the standpoint of multi-brand companies, who struggle with scaling issues and inconsistencies when having multiple teams manage content within certain branding guardrails.

In MultiLaunch, the CMS isn't a bolt-on. It’s the foundation.

This post breaks down how DatoCMS was used like a backend system to model brands, manage localization, and automate new deployments with nothing more than a slug.

MultiLaunch isn’t your average “starter kit.” It’s not a styled blog template or a landing page boilerplate. It’s a real system for launching and managing multi-brand websites, without rebuilding them over and over.

(Video content)

At the core of that system is DatoCMS. But we’re not using it the way you might typically expect. This isn’t about pages and modules and visual editors. This is about data. Structured, relational content that drives an entire frontend.

The CMS is the system. Everything else just reads from it.

### Putting the CMS first

A lot of builds treat the CMS as a dumping ground for final content. In MultiLaunch, the CMS comes first. It defines the structure. It defines the logic. The frontend reacts to it, not the other way around.

> We model the content in DatoCMS like it’s a real system. The CMS is our database. The frontend just reads from it.

That mindset shaped everything. He didn’t create pages. He created entities: `brand`, `homepage`, `theme`, `layout`, `block`, and so on.

These entities match what the business needs to manage. One brand. Many languages. Shared components. A consistent system.

(Image content)

If you poke around [the repo](https://github.com/bejamas/astro-dato-multilaunch), the structure is pretty clear. It’s a monorepo with separate apps for the core brand and individual brands. But the real foundation lives in DatoCMS.

(Video content)

There are five main models:

-   Layout handles global header and footer content.
-   Theme defines style elements like color, typography, and max width.
    
-   Homepage is built using modular blocks editors can rearrange.
-   Brand stores all the brand-level info—product, identity, SEO, etc.
    
-   Blocks are reusable sections like CTAs or reviews that power the homepage.
    

The magic is that each brand record in DatoCMS powers its own full website. Create a new record, give it a slug, hook it to a deployment, and you're live.

> You just set one variable. You have a whole new website.

### Localization from the get go

Considering that this was built with global brands in mind, MultiLaunch [supports five languages by default](https://www.datocms.com/docs/general-concepts/localization.md). Each brand in MultiLaunch can be localized into multiple languages, currently English, French, German, Dutch, and Polish.

The DatoCMS localization UI makes this painless. Content editors see all locales side by side. There’s no duplication of entries, and fields only appear once unless they need translation.

(Video content)

The best part? MultiLaunch uses the [AI Translator plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md) to auto-populate content across all languages.

> I just came in, wrote the English content, clicked translate, and it was done. That part honestly blew my mind.

And since Astro handles locale routing based on file structure, the entire multilingual frontend just works with the localized fields from DatoCMS.

### Focusing on Structured Data

The schema is relational. Everything connects back to the brand.

If a product name changes, or a brand color is updated, or a layout block gets tweaked, that change flows across all relevant components. No need to update multiple pages or deal with inconsistencies.

That’s what makes this setup fast to scale. There’s no page builder here. There’s no “drag-and-drop” layout freedom aside from well defined blocks for specific models. And that’s exactly why it works.

> We don’t let editors define layout. They define content. That keeps everything clean.

### No Overengineering

Here’s something important: MultiLaunch doesn’t use a ton of plugins. There’s no overengineering.

The CMS is basically stock. Everything in the editorial workflow is possible with built-in DatoCMS functionality, plus a few well-placed plugins like:

-   [AI Translator](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md) (for fast localization)
-   [Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) (for editors to see changes in context)
    
-   [Visual Select](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-visual-select.md) (for easily choosing layout variants)
-   [Star Rating](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-star-rating-editor.md) (because, eCommerce)
    

Mojtaba made it clear that keeping the system simple was intentional.

> We didn’t want to introduce tools just to look cool. The stock features worked. So we used them.

That simplicity makes onboarding easier, maintenance cheaper, and the system easier to scale, even as more brands are added. Speaking of scale, MultiLaunch works because it was structured with that in mind from the beginning.

It’s not a CMS setup made for a single site and then hacked into a multi-brand setup. It was modeled to support multiple brands, multiple languages, and multiple endpoints, all from one backend.

And that’s the point.

---

In Part 2, we dive into how editors actually use this setup.

We’ll show how modular content blocks work, what plugins are available, how localized content is handled, and how visual previews make publishing safer.

---

# The ol' Headless vs. WP Debate. A '25 refresher.

Source [blog]: https://www.datocms.com/blog/datocms-vs-wordpress-2025.md

Posted on [date: 2025-04-26T09:57:38.870+02:00] by Ronak Ganatra

Once upon a time (like, maybe a little less than a decade ago), the narrative was clear: headless CMS platforms like us were the slick, modern alternative to the "bloated, outdated web builder monolith" like WordPress. WordPress was for PHP developers and templated blogs; headless was for people who wanted freedom, flexibility, and performance.

We loved milking that narrative on how headless was the future for EVERYONE.

But today? Things aren’t so black and white.

And let's be honest, the WordPresses aren't going anywhere.

Many if not all classical CMS have started to blur the lines on capabilities between them and the headless ones, with better APIs, stronger feature sets, and multichannel capabilities. So let's do a little status update on the landscape, by [comparing WP to DatoCMS](https://www.datocms.com/compare/datocms-vs-wordpress.md) and see where the differences lie.

Right off the bat, credit where credit's due. WordPress now has its own headless option with WPGraphQL and REST APIs and Faust.js. There are decent solutions for content previews, scheduling, and localization. The plugin ecosystem continues to thrive. On the other side, headless platforms like us have spent the last few years seriously leveling up our editorial experience, so much so that the old “headless is painful for editors” critique just doesn’t hold up anymore.

So the question in 2025 isn’t “Is WordPress dead?”, it’s: what do you actually need from your CMS, and which tool handles that best?

And just to keep things clear: when we say WordPress here, we mostly mean classic WP (the .org PHP app with themes, templates, and plugins). Where Headless WP is relevant, we’ll call it out specifically.

Let’s break it down. We’ll keep this simple. And we'll sprinkle some 🧠 moments from chats we've had with our customers who faced the same problem before.

## TLDR:

| Feature | DatoCMS | Classic WP | Headless WP |
| --- | --- | --- | --- |
| Content modeling | Structured models, schema-safe, modular blocks | Simple posts and pages. Complex models are plugins based (ACF for example) | Plugins + WPGraphQL plugin |
| Localization | Built-in per-field, per-model. Plugins for AI/Auto translations | Plugins based | Plugins + WPGraphQL integration |
| Previews | API-based, reliable across environments | Built-in for website only | Plugins based, iframe or Faust.js previews |
| API quality | GraphQL-first, auto-generated schema | REST API (limited), no GraphQL natively | WPGraphQL plugin required, manual schema |
| Modular content | Reusable structured blocks, clean JSON | Gutenberg blocks with serialized HTML | Gutenberg blocks, or custom editor plugin |
| Permissions & workflows | Granular control, built-in workflows | Basic roles, workflows via plugins | Plugins based |
| Scaling & performance | API-first, globally distributed CDN, stack/host agnostic. | Self-managed hosting, plugins impact performance. Strategy splits between WordPress's .org and .com. | Freedom for cloud-based or self-hosted but hosted seperately (i.e. end project and CMS). |
| Developer experience | Schema-safe, DX-first, no plugin mess | PHP templates, REST API, plugin overdose | WPGraphQL improves DX compared to Classic WP |

## Content Modeling: Structured vs. “Hope This Field Works”

In WordPress, content modeling means either using the default "Posts" and "Pages" setup or extending it with popular plugins like Custom Post Types and Advanced Custom Fields (ACF). Want to make a “Case Study” type? You’re either coding that yourself or relying on ACF or a plugin that stores your data as serialized blobs inside the db. Relationships between entries? Not great. Data types? Loose at best.

Over here, [content models are first-class citizens](https://www.datocms.com/docs/content-modelling.md). Every model is explicit. Every field is typed. Relations are real relations, not just text references. Need a list of related Products on your Case Study page? That’s an actual link to the Product model, not some stringified slug in a meta field.

What this means in practice:

-   Clean GraphQL schema auto-generated from your models
-   No guessing which field contains what data
    
-   Easy to enforce content integrity across models
    

If you’re using Headless WP, you’re still stuck building your own schema via WPGraphQL. Better than nothing, but I still feel like it's forcing the tool to do something it wasn’t built for.

## Localization: Built-In vs. Plugins

Multi-language content in WordPress almost always means installing WPML, Polylang, or some other plugin. These work. Until they don’t. Performance drops. Translation status becomes messy. And content duplication happens a lot more than you’d expect.

We handle [localization at the model and field level](https://www.datocms.com/docs/general-concepts/localization.md). Any field, any model, any locale. You decide which fields are localizable. You get a clear UI showing where translations are missing. No duplication. No confusion. Want to take it a step further and automate translations with AI? There's a plugin for that. But for the very essence of localization, that's built in.

For example, if you're tweaking your global SEO settings, you can choose to localize only the title and description fields. Working on an eCommerce site and have a Product model? Localize the descriptions but keep shared product IDs and images. Simple

Even on the Headless side in WP, localization is still a plugin problem. WPGraphQL for WPML exists, but it’s yet another layer of complexity.

## Previews

Previewing unpublished content in WordPress is fine as long as you’re in the classical WP world. In fact, it's great! It's built in by default, something even we and most Headless CMS don't have without a plugin (since technically we're not just website builders). But in the headless space, previews get messy fast. You’re usually wiring up something with WPGraphQL and Faust.js or relying on iframe-based previews via plugins.

We give you flexible, API-based preview setups with the [Web Preview plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md). You configure your preview URLs per environment. Editors click “Preview” and see exactly what the content will look like on the frontend with custom breakpoints for different devices. Doesn’t matter if it’s a React site, a mobile app, or something else like a TV app.

Added bonus? Previews are tied to each of your models, so you’re not just previewing a blog post template. You can preview complex pages with modular content and dynamic relationships, no hacks needed.

## APIs and Developer Experience

One of the biggest arguments from the dev side in favor of Headless CMS have always been framework flexibility. WP boxed devs in to working with PHP whereas Headless let everyone work with whatever they prefer - Astro, Vue, Next, Svelte, whatever. No restrictions.

> So headless CMSs in general are just a lot more flexible in terms of what sort of front end you have. For example, the hoops you have to jump through to make a WordPress site headless are a lot because you must, you have to find somewhere to host your CMS for WordPress.

WordPress started as PHP templates and MySQL queries. The REST API was bolted on later. WPGraphQL? Plugin-based. Schema? You build it manually.

(Image content)

The memes hit hard.

Headless CMS on the other hand are API-first by default. Our [API is GraphQL-first, with the schema auto-generated directly from your models](https://www.datocms.com/docs/content-delivery-api.md). You get introspection. You get predictable queries. You get type safety. Every change in your content model is reflected immediately in your schema.

This is why dev teams who care about DX and reliability tend to prefer the headless option:

-   Zero plugin setup for APIs
-   CDN-backed API responses
    
-   Image transformations, responsive assets, and SEO metadata via the API
    

## Modular Content and Page Building

Gutenberg is WordPress’s block editor, but it’s really designed to output HTML directly into the frontend. This works if you’re sticking to WordPress themes. In headless setups, Gutenberg blocks get serialized and shipped via the API, but you’re manually parsing HTML in your frontend.

We do [modular content differently](https://www.datocms.com/docs/content-modelling/modular-content.md). Content blocks are structured models themselves. Your frontend gets clean JSON data, not HTML blobs. Editors can mix and match blocks like “Hero Banner,” “FAQ Section,” “Image Gallery,” or whatever your design system uses.

This ensures that you're not parsing HTML in your app and there's no weird coupling between the backend and frontend. It also gives you the full flexibility to change your rendering logic without touching the CMS, or change the layouts of your content without needing editors to get developers to change anything in the repo.

> And if you want to use a static front end with WordPress, you then need to manipulate WordPress so that when you publish a post, it will force your front end to rebuild, which WordPress, like the vanilla WordPress, is not built to do, it's not designed to do that. So you have to then reach for plugins or write your own PHP scripts to do that.

## Permissions and Workflows

WordPress user roles are basic: Admin, Editor, Author, Contributor. Want anything more fine-grained? You’re reaching for User Role Editor or a similar plugin. Another dependency. Another performance hit.

We offer [role-based access down to the field and model level](https://www.datocms.com/docs/general-concepts/roles-and-permission-system.md). Want your German marketing team to only edit German content? Easy. Want interns to create drafts but not publish? Done. Need an approval flow for certain content types? Built in.

This isn't possible in classic WP without heavy plugin customization.

## Security, Hosting, and Maintenance

One of the biggest things that quietly eats up time (and budget) with WordPress is the ongoing care and feeding required to keep things running smoothly. Security patches. Plugin updates. PHP version upgrades. Theme compatibility checks. Caching setup. Hosting performance tuning. Rinse and repeat.

If you're self-hosting WordPress, this is your responsibility. If you're on a managed host, you're paying them to handle it, but only to a point. Security features like malware scanning, backups, and DDoS protection are often part of the upsell, and not included out of the box.

> The one project where I introduced Dato to the client, who is still happy, one of his requests was, he had a different agency that did a similar thing for him, and they convinced him of self-hosting \[\*\*\*\]. And it worked quite well for a bit, but it became super slow. And then his request was, "I don't want to self-host anymore. I want headless." That was one of the first things he said to us, because it scales better for him.

Plugins also add risk. Every additional plugin is another thing that needs updating and patching. Vulnerabilities in the plugin ecosystem are one of the most common sources of WordPress hacks. And if your plugin stack isn’t maintained properly, your whole site can break on the next update.

Even in Headless WP, you're still running the full WordPress backend somewhere. That means PHP, MySQL, plugins, and all the maintenance that comes with it. Just because your frontend is React doesn’t mean you’ve escaped the WordPress server stack.

With us and most Headless options? You don’t host the CMS at all.

> For smaller clients, I think the main thing is that they don't need to maintain the CMS. It's not there. There's no overhead involved in updating and maintenance and hosting it yourself.

We handle the infrastructure, the scaling, the patching, the security. Our APIs are CDN-backed, so your content is delivered fast and globally without you needing to worry about caching plugins or server configs. You focus on building your frontend. Your editors focus on content.

[Basta](https://en.wiktionary.org/wiki/basta) 🤌

No plugins to patch. No PHP stack to maintain. No theme compatibility issues. No servers to babysit.

And if something goes wrong? You’re not chasing down which of your five caching plugins broke the site. You're just pinging us and we're solving it with you.

## What about Page Builders?

For so many users, this is really where WP's UX shines - the classic WYSIWYG approach to EVERYTHING. We can’t really talk about WordPress without talking about page builders like Elementor, Divi, or Beaver. They’re a huge part of why WordPress remains so popular today. They give content teams the power to build out pages visually, drop in widgets, and get stuff live without touching code.

But here’s the tradeoff: the HTML output from these builders is very opinionated, very bloated, and often pretty rough under the hood. You’re at the mercy of whatever markup the builder spits out, which means your frontend stack ends up buried under a pile of auto-generated `<div>` clutter.

The tool makes decisions for you, something we're personally not fans of.

To be fair, the headless world has its own versions of this. Tools like React Bricks, Builder.io, or Plasmic offer visual page building for headless setups. The key difference? Their output is structured data, not tangled HTML. Your frontend(s) still consume clean JSON and renders it the way *you* decide, not the way the builder decides.

If visual page building is important to your workflow, that’s fine, but choosing tools that respect structured content (and your frontend’s integrity) is going to save you a lot of headaches down the line.

## So. When to go Headless

I guess it all comes down to figuring out if Headless is right for you?

Here’s the simple checklist:

-   You have editors and developers who need to work alongside each other without stepping on each other’s toes. If Dev resources are really not on the table, or if you're working with highly specialized WP devs, honestly, we'd suggest sticking to that.
-   You want to use modern frameworks like React, Vue, Svelte, Astro, or plain static HTML without CMS constraints.
    
-   You’re running multiple frontends: maybe a website, maybe an app, maybe a kiosk or digital signage, and you want one content source for all of them rather than juggling multiple tools and CMS.
-   You care about scaling without stress: global CDNs, no plugin performance issues, no self-hosted CMS stack to babysit, no security patches on your side, etc. etc.
    
-   You want clean separation of concerns between content and presentation so your editors don’t need to touch code, and your developers don’t need to untangle WYSIWYG blobs
    

Compare that to the headless WordPress experience: you’re still running the full PHP app stack somewhere just to serve your API. You still need to manage plugins. You still need a server or hosting plan for WordPress itself. Even if the frontend is headless, the backend is... still WordPress.

(Image content)

With us, you don’t have to run or host the CMS at all. We handle the infrastructure, scaling, and performance for you. Your developers focus on building great frontends. Your editors focus on creating great content.

Easy.

## Where WordPress Still Wins

Ok I've 💩 enough on WP so far, but there's some very credible reasons why they're still powering like 40% of all websites. There are situations where it makes total sense:

-   Your site is small and simple: blog, landing page, contact form, done.
-   Your team knows WordPress inside out and doesn’t have developers on hand.
    
-   You need fast time-to-market without worrying about APIs or frontends.
-   Budget is limited and performance at scale isn’t a concern you want to trouble yourself with yet.
    
-   If you work with a WP-specialized agency it might be a chore and/or too expensive to swap out to a new agency or stack.
-   There's certain considerations on data portability. WP offers a clean data export which can be standardized to be imported into DatoCMS, but depending on the Headless CMS you choose, not all have that straightforward approach. This could be quite the expensive rabbit hole to go down accordingly.
    

If those points sound like you, sticking with WordPress is probably the right call.

But if your content strategy needs more than one language, more than one site, or more than one type of frontend, or if you’re tired of the plugin stack fragility, that’s [where we come in](https://www.datocms.com/contact.md).

---

# Astro, Sitemaps, SEO, and Best Practices

Source [blog]: https://www.datocms.com/blog/astro-seo-and-datocms.md

Posted on [date: 2025-01-10T13:11:24.572+01:00] by Stefano Verna

This is episode 3 of the great move-to-Astro saga for our website. This also means we're reaching the point where it's probably better to be dropping a simpler TOC for you to jump between articles if you'd like, so here's everything so far!

-   Chapter 1: [Why we switched to Astro (and why it might interest you)](https://www.datocms.com/blog/why-we-switched-to-astro.md)
-   Chapter 2: [Astro, GraphQL and DatoCMS Cache Tags: how we built the killer combo](https://www.datocms.com/blog/astro-typescript-graphql-and-datocms-cache-tags.md)
    
-   Chapter 3: Astro, Sitemaps, SEO, and Best Practices
-   Chapter 4: *Cooking*
    

In this episode, let's talk about the SEO behind the website - how we approached the sitemap, how we're dynamically generating OG images, what refactors we did to maintain internal link hygiene, and how we centralize our URL generation logic.

### Sitemaps with Glob Imports

As [we’ve mentioned in earlier articles](https://www.datocms.com/blog/why-we-switched-to-astro.md#the-new-stack), our Astro site uses the Node adapter, meaning the final output of a build is a Node.js server generating server-side responses. As a result, existing solutions like [`@astrojs/sitemap`](https://docs.astro.build/en/guides/integrations-guide/sitemap/), which are designed for statically generated routes, weren’t a fit for our needs. Yet, we wanted a solution that was as simple, intuitive, and easy to maintain.

Specifically, we aimed for a pattern where adding a new section to the site would require minimal effort — ideally just copying an existing section, tweaking it, and letting the sitemap generation logic take care of the rest.

We chose to build our sitemap as a custom endpoint using Vite's [glob imports](https://vite.dev/guide/features#glob-import). By using `import.meta.glob`, we can easily scan the project, and discover/import all the Astro routes defined.

Routes without params (ie. `/company/about/index.astro`) can be added directly to the sitemap, while for dynamic routes containing placeholders (like `/blog/[slug]/index.astro`), we used a convention: each of those route must define a `buildSitemapUrls()` function, very similar in nature to the Astro's [`getStaticPaths()`](https://docs.astro.build/en/reference/routing-reference/#getstaticpaths) function.

[Here's an example](https://github.com/datocms/astro-website/blob/main/src/pages/blog/%5Bslug%5D/_graphql.ts#L134-L150) of such function for our blog posts, querying DatoCMS for the relevant entries and generating the corresponding URLs:

src/pages/blog/\[slug\]/\_graphql.ts

```typescript
export const buildSitemapUrls: BuildSitemapUrlsFn = async (executeQueryOptions) => {
  const { entries } = await executeQueryOutsideAstro(
    graphql(
      /* GraphQL */ `
        query BuildSitemapUrls {
          entries: allBlogPosts(first: 500) {
            ...BlogPostUrlFragment
          }
        }
      `,
      [BlogPostUrlFragment],
    ),
    executeQueryOptions,
  );

  return entries.map(buildUrlForBlogPost);
};
```

This setup makes adding new sections a breeze. Just implement a similar `buildSitemapUrls()` function for the new route, and the sitemap will update automatically. It’s scalable, efficient, and fits perfectly with Astro’s modular nature.

Plus, [the code for the actual sitemap endpoint](https://github.com/datocms/astro-website/blob/main/src/pages/sitemap.xml.ts) is less than 100 lines of code!

### SEO Page Metadata

DatoCMS simplifies much of the SEO metadata generation for your website, [offering built-in tools to streamline the process](https://www.datocms.com/docs/astro/seo-management.md). By adding an SEO field to your models and querying the `_seoMetaTags` field through GraphQL, you can access a structured set of metadata that corresponds to SEO tags. Using the [`@datocms/astro` `<Seo />` component](https://github.com/datocms/astro-datocms/tree/main/src/Seo), you can effortlessly render these tags in your Astro pages.

However, some scenarios require additional customization:

-   **Pages without associated DatoCMS records:** You must manually create the metadata.
-   **Overriding metadata coming from DatoCMS:** Certain pages might need custom social sharing images or specially crafted titles/descriptions. Instead of asking content editors to provide these manually, you can generate them programmatically. It's the case of pages like our [Agency Partner profiles](https://www.datocms.com/partners/november-five.md).
    

To address these needs, we developed [a set of utility functions](https://github.com/datocms/astro-website/blob/main/src/lib/datocms/seo.ts) allowing to modify metadata fetched from DatoCMS, or even build metadata from scratch for pages without associated records. This ensures a consistent approach across the site. This snippet is taken from the [code of our Agency Partner profile component](https://github.com/datocms/astro-website/blob/main/src/pages/partners/%5BpartnerSlug%5D/index.astro#L47-L58) and shows how we use them:

```jsx
---
import { overrideSeo, seoDescription, seoGeneratedCard, seoPageTitle, seoShareTitle } from '~/lib/datocms/seo';

const query = graphql(
  /* GraphQL */ `
    query PartnerQuery($partnerSlug: String!) {
      page: partner(filter: { slug: { eq: $partnerSlug } }) {
        _seoMetaTags {
          ...TagFragment
        }
        # other data required for the page
      }
    }
  `,
  [TagFragment]
);

const { page } = await executeQuery(Astro, query, { variables: { partnerSlug: Astro.params.partnerSlug! } });

if (!page) {
  return notFoundResponse();
}

---

<Layout
  seo={overrideSeo(
    // start from SEO metadata coming from the record...
    page._seoMetaTags,
    // and then override some SEO tags:
    seoPageTitle(page.name, 'DatoCMS Partners'),
    seoShareTitle(`DatoCMS Partner: ${page.name}`),
    seoDescription(page.shortDescription),
    seoGeneratedCard(Astro, {
      kicker: `DatoCMS Agency Partners: ${page.name}`,
      excerpt: page.shortDescription,
      pills: [`${page.projects.length} showcased projects`],
      logoPngUrl: page.logo.pngUrl,
    }),
  )}
>
  {/* Page content */}
</Layout>;
```

### Dynamic OG Card Images

You might have spotted an interesting `seoGeneratedCard()` function in the snippet above — one of the coolest features we've implemented is a dynamic OG card generator. We created an [Astro endpoint](https://github.com/datocms/astro-website/blob/main/src/pages/og-card/index.png.ts) that accepts configuration parameters through a base64-encoded JSON string and returns custom Open Graph images:

```plaintext
https://www.datocms.com/og-card.png?data=<base64(JSON.stringify(configuration))>
```

The configuration object supports several parameters that let us create rich, dynamic preview cards. Here's a real example we use for our partner pages:

```json
{
  "kicker": "DatoCMS Agency Partners: November Five",
  "excerpt": "MADE TO MOVE\\n\\nDigital solution partner for experience minded leaders.",
  "pills": ["2 showcased projects"],
  "logoPngUrl": "https://www.datocms-assets.com/205/1694523317-logo_n5.svg"
}
```

And this are a couple of examples of the final result:

(Image content)

Under the hood, we're using Vercel's fantastic [`satori`](https://github.com/vercel/satori) library combined with [`sharp`](https://github.com/lovell/sharp) for image processing. Satori is a game-changer — without having to spin up heavy headless browsers, it can convert HTML and CSS into SVG files which we then transform into stunning PNG images. 💫

The best part? Since these images are generated on-demand, they're always perfectly synchronized with your content. No more outdated social previews!

### Keeping Links at Bay

If you’ve managed a medium-to-large website, you know the struggle: invalid links cropping up over time, breaking the user experience and tanking your SEO efforts. Like many others, we had accumulated [hundreds of scary redirect rules](https://github.com/datocms/new-website/blob/master/redirects.js) over the years to patch the issue.

With this site rewrite, we decided to eliminate the problem at its source, leveraging DatoCMS’s built-in tools and a bit of automation.

##### The power of Referential Integrity in DatoCMS

DatoCMS’s ["Link to records" functionality in Structured Text fields](https://www.datocms.com/docs/content-modelling/structured-text.md#linking-records) is a game-changer. Instead of relying on fragile hardcoded URLs, we now link directly to records within the CMS.

(Video content)

This approach, which we call **Content Referential Integrity**, ensures that relationships between content pieces are always valid and consistent.

Here’s why this is so powerful:

1.  **Validity of References:** Links between records are validated automatically. If a record is deleted or becomes inaccessible, DatoCMS ensures that no dangling references remain.
    
2.  **Error Prevention:** Records with dependencies cannot be deleted without resolving those dependencies first. For example, if a blog post links to another record, you’ll be prompted to update or remove the reference before deletion.
    
3.  **Data Consistency:** Changes to a referenced record — like its URL or identifier — are immediately reflected in all associated content. This eliminates the need to manually track and update links across the site.
    

##### Automation to ensure link accuracy

We took this one step further by building [a bot powered by DatoCMS webhooks](https://github.com/datocms/astro-website/blob/main/src/pages/api/normalize-structured-text/index.ts). This bot listens for changes to Structured Text fields and automatically replaces internal textual links with direct references to records when possible. It also prevents editors from adding links to non-existent internal pages, enforcing continued integrity of our content:

(Video content)

What you're seeing in the video is the Structured Text field being updated by adding an internal URL to a datocms.com page and being saved. Right after, the bot does its thing and converts it into an in-line link matching the corresponding record to ensure future integrity if and when the slug were to change.

##### Centralized URL generation

Another key to maintaining long-term consistency was centralizing the logic for URL generation. Instead of scattering hardcoded URLs across components, we built a reusable system for deriving URLs from records.

Here’s an example of how we handle [blog post URLs](https://github.com/datocms/astro-website/blob/main/src/lib/datocms/gqlUrlBuilder/blogPost.ts):

src/lib/datocms/gqlUrlBuilder/blogPost.ts

```typescript
// GraphQL fragment for retrieving the required data
export const BlogPostUrlFragment = graphql(/* GraphQL */ `
  fragment BlogPostUrlFragment on BlogPostRecord {
    slug
  }
`);

// Helper function for building URLs
export function buildUrlForBlogPost(blogPost: FragmentOf<typeof BlogPostUrlFragment>) {
  const data = readFragment(BlogPostUrlFragment, blogPost);
  return `/blog/${data.slug}`;
}
```

This centralized approach ensures consistency, even as the data model evolves. Need to change the structure of blog post URLs? Update it in one place, and the change propagates across the entire codebase.

##### The Results

Setting up this system required some initial effort, but the benefits have been substantial. By eliminating hardcoded links, automating referential checks, and centralizing URL logic, we’ve ensured that our site remains resilient, maintainable, and SEO-friendly over time.

And that's it for this one! Stay tuned for when the next episode drops ✌️

---

# Astro, GraphQL and DatoCMS Cache Tags: how we built the killer combo

Source [blog]: https://www.datocms.com/blog/astro-typescript-graphql-and-datocms-cache-tags.md

Posted on [date: 2024-12-12T12:50:27.133+01:00] by Stefano Verna

*This is the second episode of our exciting saga regarding the complete rewrite of our site in Astro. If you haven't had the chance to read* [*the first episode*](https://www.datocms.com/blog/why-we-switched-to-astro.md)*, we highly recommend you do so, as it lays the groundwork and context for what we're diving into today!*

-   Chapter 1: [Why we switched to Astro (and why it might interest you)](https://www.datocms.com/blog/why-we-switched-to-astro.md)
-   Chapter 2: Astro, GraphQL and DatoCMS Cache Tags: how we built the killer combo
    
-   Chapter 3: [Astro, Sitemaps, SEO, and Best Practices](https://datocms.com/blog/astro-seo-and-datocms)
-   Chapter 4: *Cooking...*
    

In this episode, we will talk about one of the most critical and fundamental topics that are essential for a DatoCMS-powered Astro website: how to effectively work with GraphQL.

## A simple pattern to obtain the data

The vast majority of our site's pages primarily execute GraphQL queries to DatoCMS and convert them into Astro pages. Due to the frequency of this process, it was essential to establish the **simplest possible pattern** — ideally, a single function call. This is what we came up with ([source code](https://github.com/datocms/astro-website/blob/main/src/pages/product-updates/%5Bslug%5D/index.astro)):

src/pages/product-updates/\[slug\]/index.astro

```typescript
import { executeQuery } from '~/lib/datocms/executeQuery';
import { notFoundResponse } from '~/lib/notFoundResponse';
import { query } from './_graphql';

const variables = { slug: Astro.params.slug! };
const { productUpdate } = await executeQuery(Astro, query, { variables });

if (!productUpdate) {
  return notFoundResponse();
}

---

<Layout>
  {JSON.stringify(productUpdate)}
</Layout>
```

The [`executeQuery` function](https://github.com/datocms/astro-website/blob/main/src/lib/datocms/executeQuery.ts) does more than just executing the query and returning the result:

-   Checks if the domain of the request is `www` or `www-draft`, and sets the [`X-Include-Drafts` header](https://www.datocms.com/docs/content-delivery-api/api-endpoints.md#preview-mode-to-retrieve-draft-content) accordingly to return draft content or not;
-   Sets the [`X-Cache-Tags` header](https://www.datocms.com/docs/content-delivery-api/api-endpoints.md#cache-tags) to `true` to obtain the cache tags associated with the GraphQL response;
    
-   Copies the cache tags obtained from DatoCMS into the [`Surrogate-Key`](https://www.fastly.com/documentation/reference/http/http-headers/Surrogate-Key/) header of the Astro response ([source code](https://github.com/datocms/astro-website/blob/main/src/lib/surrogateKeys.ts#L12)). An important consideration is that each Astro component involved in rendering a page may trigger a different GraphQL query, meaning the `Surrogate-Key` header **must be a union** of all cache tags returned by individual `executeQuery` invocations.
    

The global `Astro` carries all the necessary context for these purposes (i.e. the request and response objects), so it is the only additional argument to pass, besides the GraphQL query and its variables. Fantastic!

##### Strict Mode for the DatoCMS CDA

When working with TypeScript, remember to always enable [Strict Mode](https://www.datocms.com/docs/content-delivery-api/api-endpoints.md#strict-mode-for-non-nullable-graphql-types) when sending GraphQL requests to DatoCMS.

By adding the `X-Exclude-Invalid` header, you basically filter out invalid records from GraphQL responses, AND enable more precise and reliable TypeScript types that guarantee non-null, correctly validated data fields across your models.

(In our case, the header [is set by default](https://github.com/datocms/astro-website/blob/main/src/lib/datocms/executeQuery.ts#L42) by our `executeQuery()` function)

## Caching on Fastly

Let's analyze the headers that our `executeQuery()` sets on Astro's responses. If the domain of the request is `www`, the headers will be:

```plaintext
Surrogate-Key: <LIST OF CACHE TAGS>
Surrogate-Control: max-age=31536000, stale-while-revalidate=60, stale-if-error=86400
```

The `Surrogate-Control` header instructs Fastly to cache the response for one year. In addition to the classic `max-age`, we also use the [`stale-while-revalidate`](https://web.dev/articles/stale-while-revalidate) and `stale-if-error` directives, which are very powerful:

-   `stale-while-revalidate=60` means that for up to one minute after the cache expires, Fastly can **immediately serve the stale content while asynchronously fetching a fresh version in the background**. This means that visitors do not pay the price of a slower response even in the case of invalidated cache.
-   The `stale-if-error=86400` allows Fastly to continue serving the cached content for up to 24 hours if the origin server is unavailable or returns an error, ensuring **better availability** and user experience during potential server issues.
    

On the `www-draft` domain, we do not have Fastly in front, so we are simply concerned with exposing the cache tags for debugging purposes, and instructing browsers to never cache the server responses — we always want the latest data:

```plaintext
Debug-Surrogate-Key: <LIST OF CACHE TAGS>
Cache-control: private
```

## Invalidating Fastly's cache

Since our pages will be cached by Fastly for a year, it’s time to focus on invalidating them!

I absolutely love this part every time I work with DatoCMS: [setting up a webhook](https://www.datocms.com/docs/content-delivery-api/cache-tags.md#step-3-implement-the-invalidate-cache-tag-webhook) from the interface, writing just 20 lines of code, and accomplishing what previously would have taken **weeks of optimization and debugging** to invalidate pages correctly! 😅

src/pages/api/cache-tags-invalidate/index.ts

```typescript
import type { APIRoute } from 'astro';
import { json } from '../_utils';
import { FASTLY_KEY, FASTLY_SERVICE_ID } from 'astro:env/server';
import ky from 'ky';

export const POST: APIRoute = async ({ request }) => {
  const data = await request.json();

  // DatoCMS sends us the tags to be invalidated via webhook
  const cacheTags = data.entity.attributes.tags;

  const response = await ky.post(`https://api.fastly.com/service/${FASTLY_SERVICE_ID}/purge`, {
    headers: {
      'fastly-key': FASTLY_KEY,
      // Required for stale-while-revalidate to work!
      'fastly-soft-purge': '1',
      'content-type': 'application/json',
    },
    json: { surrogate_keys: keys },
  }).json();

  return json({ cacheTags, response });
};
```

Man, I'm so proud of our [Cache Tags](https://www.datocms.com/blog/introducing-datocms-cache-tags.md). 🥰

## GraphQL and TypeScript

One of the objectives of the rewrite was to achieve complete TypeScript coverage. For this purpose, we chose to use [gql.tada](https://gql-tada.0no.co/), an incredible library capable of **deriving the types for your GraphQL queries on the fly**.

We have chosen to organize our routes this way and to declare the GraphQL queries in a `_graphql.ts` file:

(Image content)

Files with the \_ prefix won’t be recognized by the Astro router

The following is an example of a query. Once passed as an argument to our `executeQuery()`, the result will be fully typed!

src/pages/product-updates/\[slug\]/\_graphql.ts

```typescript
import { ProductUpdateFragment } from '~/components/product-updates/ProductUpdate/graphql';
import { TagFragment } from '~/lib/datocms/commonFragments';
import { graphql } from '~/lib/datocms/graphql';

export const query = graphql(
  /* GraphQL */ `
    query ProductUpdate($slug: String!) {
      productUpdate: changelogEntry(filter: { slug: { eq: $slug } }) {
        _seoMetaTags {
          ...TagFragment
        }
        ...ProductUpdateFragment
      }
    }
  `,
  [TagFragment, ProductUpdateFragment],
);
```

## Fragment composition

One of the less talked about strengths of GraphQL, when working in "componentized frameworks" like Astro, React, Vue or Svelte, lies in fragment composition and hierarchical schema design. The gql.tada documentation [does a great job of explaining this pattern and its benefits](https://gql-tada.0no.co/guides/fragment-colocation).

Basically, each component defines its own data requirements using a GraphQL fragment stored in a `graphql.ts` file next to the component itself:

(Image content)

The directory structure of every component in the project

When a parent component includes child components, it aggregates their GraphQL fragments. Here's how the process unfolds for a `<Parent />` component:

-   The parent imports both the child component (e.g., `<QuestionAnswer />`) and its corresponding GraphQL fragment (e.g. `QuestionAnswerFragment`).
-   It then creates its own fragment by declaring its specific data requirements and incorporating the fragments of its child components.
    

This fragment composition continues up the component tree until it reaches the top-level Astro page, which will then execute a single, comprehensive combined macro-query.

It's an incredibly powerful pattern: it creates a **modular, clean, maintainable way of managing component data requirements in a type-safe, composable manner**, enforcing the Single Responsibility Principle and Separation of Concerns. Regardless of how many pages use your component, future changes will require modifications in only one place in your code.

If there's only one takeaway from this article, it's this: use fragment composition!

## Simplifying GraphQL Pagination

Sometimes website sections require displaying a large volume of content. A perfect example is our [Wall of Love ♥️](https://www.datocms.com/wall.md) , where the goal is to create a wow effect by showcasing numerous quotes, without necessarily expecting visitors to read each one carefully. In such cases, pagination becomes crucial for retrieving entire collections of records.

GraphQL pagination is notoriously challenging — it's rarely as straightforward as developers would like, and especially its logic, unlike REST APIs, is much more difficult to extract and reuse across different parts of your application.

However, it's not impossible! Recently, we've added an ingenious method to our GraphQL client `@datocms/cda-client` that simplifies this entire process called [`executeQueryWithAutoPagination`](https://github.com/datocms/cda-client/tree/main?tab=readme-ov-file#executequerywithautopagination):

```typescript
import { executeQueryWithAutoPagination } from "@datocms/cda-client";

const { allQuotes } = await executeQueryWithAutoPagination(`
  query WallOfLove {
    allQuotes(first: 5000) { author quote }
  }
`);
```

What should jump out at you from this piece of code is that we're fetching 5,000 records in a single call — which seems impossible, given that at the moment, pages with DatoCMS are [limited to 100 elements](https://www.datocms.com/docs/content-delivery-api/pagination.md)!

Well, `executeQueryWithAutoPagination` automatically analyzes the query and dynamically rewrites it behind the scenes, like this:

```graphql
query WallOfLove {
  splitted_0_allQuotes(first: 100, skip: 0) { author quote }
  splitted_1_allQuotes(first: 100, skip: 100) { author quote }
  splitted_2_allQuotes(first: 100, skip: 200) { author quote }
  # ... and so on
}
```

Once executed, the results are seamlessly collected and recomposed, **making the entire pagination process transparent to the developer**, and without requiring multiple calls. It's like magic! ✨

## Wrap-up

We hope that sharing our site's data fetching strategies gives you some inspiration for a modern, efficient approach to web development, even if you're using different frameworks than Astro:

-   we’ve implemented robust content delivery with Fastly caching, and at the same time streamlined route query complexity to a single function call, so that no one can make unintentional errors;
-   we've built a modular and mantainable GraphQL design with component-level fragments and bottom-up fragment composition.
    
-   everything is supported by TypeScript, so it becomes immediately clear if any GraphQL query is incorrect (or becomes incorrect in the future due to a schema change).
    

Stay tuned for the next episode of our saga, where we'll dive deep into other areas of our website! 👋🏼

---

# Why we switched to Astro (and why it might interest you)

Source [blog]: https://www.datocms.com/blog/why-we-switched-to-astro.md

Posted on [date: 2024-12-03T15:41:38.956+01:00] by Stefano Verna

This is part one of an ongoing series on moving our website to Astro! Here's what we've put together so far:

-   Chapter 1: Why we switched to Astro (and why it might interest you)
-   Chapter 2: [Astro, GraphQL and DatoCMS Cache Tags: how we built the killer combo](https://www.datocms.com/blog/astro-typescript-graphql-and-datocms-cache-tags.md)
    
-   Chapter 3: [Astro, Sitemaps, SEO, and Best Practices](https://www.datocms.com/blog/astro-seo-and-datocms.md)
-   Chapter 4: *Cooking*
    

If everything went according to plan, no one should have noticed anything. But for a few days now, this site has been completely new.

**We took our old Next 13 site and completely rewrote it in Astro! 🧑‍🚀🚀**

Not only that: we also moved from the classic Vercel hosting to a completely server-side rendering approach on a VPS, which allows our team an editing experience with immediate feedback, combined with a blazing-fast CDN for the website visitors.

Astro was a bet. We didn't know it well enough to be sure everything would go as expected. But **the result was a complete success from every perspective**, and the development experience was extremely educational and — not something to be taken for granted — *fun*.

The final architecture offers performance and consumption typical of a static site, coupled with instant and granular page-level invalidation thanks to our [Cache Tags](https://www.datocms.com/blog/introducing-datocms-cache-tags.md). The dream of every website.

This is part one of a series of articles in which we'll try to summarize our journey, sharing many cool little Astro details we discovered (or totally came up with) in the process. We hope they can be useful to those, who like us, manage a content-driven site and would like to consider an alternative to the classic Next/Nuxt/Svelte + Vercel/Netlify stack.

Now let’s be clear – by no means are we suggesting we’re *against* Next.js. Heck, we’re incredibly proud of the official [Next.js conference starter](http://next.js/) which was originally built with DatoCMS in 2020! However, considering our content and sitemap, several factors came into play to make this decision.

*PS: The second part of the series is where we dive into using GraphQL Strict Mode, DatoCMS Cache Tags, gql.tada, and lots of GraphQL. This is also live* [*on our blog*](https://www.datocms.com/blog/astro-typescript-graphql-and-datocms-cache-tags.md) *so open that up in a new tab to dive in to after this one!*

## Why did you do it?

There are many reasons why we needed to make BIG changes to our website.

##### TypeScript

Our Next site was a project born in 2019: five years in the magical frontend world is a geological era (unfortunately). At the time, there were no reliable typed GraphQL schema generators. The result was that the old site was written in pure JS, without TypeScript. Over time, as pages increased, we felt less and less comfortable making big modifications with the confidence of not breaking anything.

Hence the first objective: **regain confidence by switching to a complete TypeScript codebase**, where every GraphQL query would be fully typed. But since that meant rewriting a good chunk of our code anyways, why not consider alternatives to Next.js?

##### Eating our own dog food

Our product, DatoCMS, has changed A LOT since 2019. Yet our Next site wasn't leveraging some of the coolest features we've released in recent times, like [GraphQL "Strict Mode"](https://www.datocms.com/blog/introducing-strict-mode-for-graphql-cda-get-the-best-typescript-dx.md) — which pairs wonderfully with TypeScript — but especially [Cache Tags](https://www.datocms.com/blog/introducing-datocms-cache-tags.md), which can offer top-notch performance through **completely static and cached content**, while still letting visitors access the latest version of any page, seconds after changes have been published.

We wanted a site that represented **the state of the art of what DatoCMS can offer**.

##### A simpler mental model

In 2019, Next.js introduced [`getStaticProps()`](https://nextjs.org/blog/next-9-3#next-gen-static-site-generation-ssg-support) along with [Draft Mode](https://nextjs.org/blog/next-9-3#preview-mode) — a revolutionary hybrid static/dynamic approach that was a breath of fresh air compared to our former Gatsby setup. The developer experience was delightful and straightforward.

Fast forward to today, and the React ecosystem has transformed dramatically. Setting up React is now so complex that React itself [recommends using it exclusively through a framework](https://react.dev/learn/start-a-new-react-project). Next.js and React have become almost indistinguishable, to the point where, at the time of its public launch, Next.js 15 was using... an unreleased React release candidate?

Starting with Next.js 14, the introduction of the app router, Server Components, and Incremental Regeneration (ISR) have **exponentially increased complexity**. Our team frequently encountered bewildering results and errors, with Next.js documentation offering little clarity about the underlying mechanics. We're not alone in this frustration — many developers are questioning the framework's direction (i.e. [Why I Won't Use Next.js](https://www.epicweb.dev/why-i-wont-use-nextjs)).

While Next.js 15 addresses some issues, and is an incredibly powerful framework, it feels like React/Next is moving in a direction that isn't ours. **For a content-driven site with static pages that look the same to every visitor, these increasingly sophisticated solutions feel like overkill** and force us into unnecessary complexity.

Hence the last objectives:

-   **return to a simple mental model** that doesn't require a frontend degree to navigate without making massive errors.
-   **design a simple, standards-based architecture** that offers maximum control and remains cost-effective.
    

## Why Astro?

With these premises, choosing Astro as the reference framework was quite an obvious consequence.

To use DatoCMS Cache Tags, the only requirement at the "engine" level of the website is to offer a server-side rendering mode capable of freely manipulating the headers of each page's response, and [Astro checks the box](https://docs.astro.build/en/reference/api-reference/). This is certainly not a strict requirement: Next, Nuxt, SvelteKit... are all frameworks that offer server-side rendering.

The critical distinction lies in their core architectures. Next, Nuxt, and SvelteKit are built with complex, runtime browser rendering engines — a massive overhead for content-driven websites with minimal interactive elements. This approach introduces unnecessary mental overhead for developers, forcing them to constantly juggle the cognitive load of writing code that must execute flawlessly in both server and browser environments. It also increases the overall computational complexity and page size for the final visitor.

Astro takes a bold and clear position: it is firmly focused on server-side and static generation. Unlike its competitors, **Astro categorically refuses browser-side rendering**. It doesn't just minimize client-side rendering — it eliminates it entirely. And this goes exactly towards our goal: a simple mental model.

> Wait, why not just use PHP then? 👀

Because web development isn't black and white. No site can be completely absent of JavaScript — *certainly not ours!* And when you need to break free from pure server-side rendering and add interactive elements, Astro provides a seamless solution that PHP simply cannot match.

Having Vite as its engine, Astro allows you to effortlessly insert JavaScript code, handling all the bundling for you. Moreover: if for particular areas of the page you need strong browser-side interactivity, Astro allows you to insert entire [interactive "islands"](https://docs.astro.build/en/concepts/islands/) of React/Vue/Svelte, limiting hydration only to those specific parts.

That's exactly what we, and most content-driven websites, need: the performance and simplicity of server-side rendering for the vast majority of the pages, combined with targeted, rich interactivity where necessary.

*(Also, as you may recall, one of our goals was to incorporate a form of safety net through typed languages. PHP/Ruby/Python are not inherently typed, plus lack advanced tools like TypeScript for automatically managing the typing of GraphQL queries. So yeah, PHP will need to take a backseat for now.* 🤷‍♂️*)*

## The new stack

(Image content)

Wow, that's a lot of arrows! (14 to be precise)

Let's analyze it point by point what we came out with:

-   The new site is written in Astro and uses the [Node adapter](https://docs.astro.build/en/guides/integrations-guide/node/), meaning the final output of a build is a **Node.js server** that needs to run on a physical server. Astro generates new server-side responses for each incoming request.
-   The server in question is a VPS on [Hetzner](https://www.hetzner.com/). The app deployment is managed by [Kamal](https://kamal-deploy.org/), and can occur manually from the command line or via GitHub Actions. With Hetzner's outrageous prices, you can acquire all the necessary hardware for the task, and more, for just €15/month.
    
-   The domain `www-draft.datocms.com` points directly to the Astro server.
-   The domain `www.datocms.com`, on the other hand, points to Fastly, a CDN that supports [surrogate keys](https://docs.fastly.com/en/guides/working-with-surrogate-keys). Fastly uses `www-draft.datocms.com` as the origin.
    
-   Astro pages, depending on whether the request host is either `www-draft` or `www`, will execute GraphQL requests to DatoCMS with the [`X-Include-Drafts` header](https://www.datocms.com/docs/content-delivery-api/api-endpoints.md#preview-mode-to-retrieve-draft-content) either active or not. This allows our team to access draft content while regular visitors do not.
-   Astro also leverages [DatoCMS Cache Tags](https://www.datocms.com/blog/introducing-datocms-cache-tags.md) to cache pages on Fastly indefinitely. In practical terms, Astro reads the `X-Cache-Tags` header for each GraphQL request made to DatoCMS, and applies those tags identically in its own response via the `Surrogate-Key` header.
    

There's only one missing piece: cache invalidation. DatoCMS, through webhooks, sends cache invalidation tags to the Astro server with every content change on the CMS. Astro then uses those tags to [purge the Fastly cache](https://docs.fastly.com/en/guides/purging-with-surrogate-keys) via an API call:

(Image content)

Cache invalidation via DatoCMS Webhooks

## How did it go?

Honestly, **we cannot be happier** about the final result:

-   Astro's support for TypeScript is excellent, and in general, it has allowed us to do everything we needed, even when we had to go outside "the norm".
-   The mental model of the architecture is extremely simple: **everything's server-side generated. Period.** No re-hydration, no huge client-side JavaScript bundles.
    
-   Our team can work on the content and see the results in real-time as they save their drafts through [Web Previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) and [Real-time Updates](https://www.datocms.com/docs/astro/real-time-updates.md).
-   End visitors always get cached content from CDN (thanks to [`stale-while-revalidate`](https://httpwg.org/specs/rfc5861.html#rfc.section.3)), so the **response is immediate**.
    
-   Thanks to cache tags, **cache purging is laser-precise**, ensuring that only the pages that truly require updates are invalidated...
-   ...yet, developers do not need to take any action to manage all the traditional complexity of caching. Once again, the mental model is straightforward, to the extent that **you can forget about it**.
    
-   Thanks to Fastly, **cache purging happens in less than 150ms**: if we accidentally publish incorrect content, we can correct it instantly, without having to wait minutes for a build to complete.
-   Complete builds and cache purging only occur when a new version of the repo is pushed. In our case, the **total build time is approximately 3 minutes**. We can probably improve this if we commit ourselves... the important feature, however, is that this time is **independent of the number of pages** on the site.
    
-   Last, but not least: **hosting costs are ridiculously low**.
    

## How bad was the actual migration?

Our site is about a hundred different sections: from the beginning of the first tests to the full release, approximately **two and a half months** have passed. Not bad, considering it was never a full-time commitment, and that it has only been [Marco](https://www.marcomezzavilla.com/) and me working on it.

Astro's [JSX-like syntax](https://docs.astro.build/en/basics/astro-syntax/) made it pretty easy to transition all the components. Only the more complex components have remained in React as [Astro Islands](https://docs.astro.build/en/concepts/islands/), while small interactivity (`useState`, `useEffect`, etc.) has been **converted into Web Components** fairly easily.

## "I want the juicy details!"

Good, because we can't wait to talk to you about it! 😜 After this first introduction to the overall architecture of the new site, the upcoming articles will explore in detail many practical topics related to Astro in conjunction with DatoCMS, and how we achieved clear, clean, and highly maintainable code.

These are just a few of the topics we want to discuss:

-   Typed GraphQL schema;
-   GraphQL Fragment Colocation;
    
-   URL Building / SEO Management / Sitemaps;
-   Error management;
    
-   Astro Actions;
-   Replacing React with Web Components;
    
-   View Transitions;
-   SVGs;
    

Whether you're considering a similar migration or just curious about modern web development best practices, we hope our journey inspires and informs yours. See you in the [next episode](https://www.datocms.com/blog/astro-typescript-graphql-and-datocms-cache-tags.md)! 👋

---

# Comparing JS frameworks for content-heavy sites

Source [blog]: https://www.datocms.com/blog/comparing-js-frameworks-for-content-heavy-sites.md

Posted on [date: 2024-11-13T14:06:24.000+01:00] by Ronak Ganatra

Ok, let’s get right into it - no long life story or filler.

“*Which SSG/frontend framework do you recommend when working with DatoCMS*” is a supremely common conversation we have with our users, and while the classic answer is always “it depends”, I’m entitled to form overly subjective opinions based on several criteria. Especially given that *most* of these conversations revolve around content heavy websites that have 1000s and 1000s of articles.

So. I spun up 10,001 Lorem Ipsum blog posts in a DatoCMS project, used nothing but the basics (no fancy CSS frameworks, no animations, no overkill) and threw together a simple frontend to stress test Next.js, Nuxt, Svelte, and Astro head to head.

This is what we built with each framework.

-   With NextJS: [https://datocms-blog-with-nextjs.vercel.app/](https://datocms-blog-with-nextjs.vercel.app/)
-   With NuxtJS: [https://datocms-blog-with-nuxtjs.vercel.app/](https://datocms-blog-with-nuxtjs.vercel.app/)
    
-   With Astro: [https://datocms-blog-with-astro.vercel.app/](https://datocms-blog-with-astro.vercel.app/)
-   With SvelteKit: [https://datocms-blog-with-svelte.vercel.app/](https://datocms-blog-with-svelte.vercel.app/)
    

Why bother comparing these in the first place? Well they’re currently the most commonly discussed for *us* so we wanted to lay out what we think works best, but if you’re using or prefer something else (ember.js or some bleeding edge `tanstack start` anyone?), you do you!

It's also worth noting, that while this definitely isn't meant to be "best practice", we opted to take the default Astro approach of prefetching and prerendering EVERY path, so build times bloated up while we built 10,001+ slugs on build time. While this may not be the best approach for different use-cases, we took it this time around to try and be a bit more cache efficient since we're not adding any cache invalidation or headers in addition.

**TLDR? I (very personally and subjectively) ended up claiming that Astro was the best framework for content-focused websites.**

Anyways, here’s everything we covered 👇

## The CMS stuff

In DatoCMS we set up an oversimplified model for blog posts. The homepage and blog index page is handled directly in the repo, so all we needed to do was provide an API with the “content” itself.

In the project we have 2 models, one for a blog post, and one for an author related to the blog post.

(Image content)

The author model is a simple `string` field for the name, and an `asset` field for the avatar.

The post model is slightly more complicated to try and use multiple field types:

-   An `asset` field for a featured image
-   `String` fields for the title and description
    
-   A `Structured Text` field for the content
-   A `relation` field to connect a post to an author
    
-   A `slug` field for the slug, and
-   A `date` field for the publication date
    

(Image content)

Finally, in the media area we added 4 avatars and 4 stock images to use in rotation for all the posts, before Lorem Ipsumming 10K posts using the [Content Management API](https://www.datocms.com/docs/content-management-api/resources/item/create.md).

To confirm that everything was working as expected, we played around with the [CDA playground](https://www.datocms.com/docs/content-delivery-api.md) to make sure the only 2 queries we needed were returning all the expected content.

Querying for all the posts to list out on the /blog page

```graphql
{
  allPosts(first: 100) {
    title
    slug
    date
    author {
      name
    }
  }
}
```

Which returned something like this

```json
"data": {
    "allPosts": [
      {
        "title": "Quisque. Fringilla pharetra metus ante natoque mattis lacus faucibus nisl.",
        "slug": "quisque-fringilla-pharetra-metus-ante-natoque-mattis-lacus-faucibus-nisl",
        "date": "2024-10-31",
        "author": {
          "name": "Tim"
        }
      },
}
…
```

Querying for each post by slug to generate the /blog/\[slug\] pages

```graphql
query getPost($slug: String) {
  post(filter: {slug: {eq: $slug}}) {
    title
    slug
    image {
      url
      id
    }
    date
    description
    content {
      value
    }
    author {
      name
      avatar {
        url
        id
      }
    }
  }
}
```

Which returned something like this

```json
{
  "data": {
    "post": {
      "title": "Quisque. Fringilla pharetra metus ante natoque mattis lacus faucibus nisl.",
      "slug": "quisque-fringilla-pharetra-metus-ante-natoque-mattis-lacus-faucibus-nisl",
      "image": {
        "url": "https://www.datocms-assets.com/144276/1729501777-bike.avif",
        "id": "UcjRUwtiS5ujFm3PN6vqqg"
      },
      "date": "2024-10-31",
      "description": "Scelerisque molestie posuere varius. Senectus Massa. Eros. Taciti auctor sagittis risus nostra pellentesque morbi lacus vivamus magna rutrum nisl tempor.",
      "content": {
        "value": {
          "schema": "dast",
          "document": {
            "type": "root",
            "children": [
              {
                "type": "heading",
                "level": 2,
                "children": [
                  {
                    "type": "span",
                    "value": "Netus"
                  }
                ]
              },
              //...
            ]
          }
        }
      },
      "author": {
        "name": "Makenna",
        "avatar": {
          "url": "https://www.datocms-assets.com/144276/1729500256-raul.avif",
          "id": "fNh4cWO6TreUpLtRTesr8w"
        }
      }
    }
  }
}
```

Once we were sure the CMS stuff works, off we went into the barebones frontends using each framework.

## The “Design”

To keep things light, we ended up not choosing any fancy CSS framework or complex styling. Between Tailwind, Chakra, Blaze, and [so many others](https://github.com/troxler/awesome-css-frameworks?tab=readme-ov-file), it's a highly subjective matter anyways, so we opted to have a consistent `globals.css` shared between all projects, and snuck in a few cheeky inline CSS params for some sections (mostly out of laziness than anything else).

The end result was a very Halloween-ey looking black and orange blog that won't win any design awards.

(Image content)

The posts themselves were just rendered using DatoCMS's structured text packages for [React](https://github.com/datocms/react-datocms), [Vue](https://github.com/datocms/vue-datocms), and [Astro](https://github.com/datocms/astro-datocms) (with the exception of Svelte which uses structured-text-to-html-strings). These too had no added styling, just rendering the content with a bit of padding here and there, and not applying any additional asset optimizations.

## Working with each Framework

I've previously played around with Next and Astro before, so they were quicker to set up, but given the great documentation and community, and I had no complaints diving into Vue and Svelte as well. From the DX side of things, I guess, this is where a "work with what you like" is more valid than anything else, there wasn't one framework that did anything dramatically different or out of convention.

### With Next.js

We took a very simple approach getting started with a `npx create-next-app@latest next-demo`, and opted to use the app router, without TypeScript or Tailwind, since we didn't want to bring in any heavy CSS into the project.

This led to a rather minimal list of core dependencies, with as little "bloat" as Next would allow in a setup like this.

```plaintext
├── @babel/core@7.25.8
├── @datocms/cda-client@0.2.2
├── babel-eslint@10.1.0
├── dotenv@16.4.5
├── eslint-config-next@15.0.0
├── eslint@8.57.1
├── next@15.0.0
├── react-datocms@7.0.3
├── react-dom@18.3.1
└── react@18.3.1
```

Next offers Geist as a local font out of the box, so we retained it - with the other frameworks defaulting to system fonts.

Our structure was obscenely minimal, with a component created for a Nav, a homepage, and a blog page and post page nested under a blog directory, all using a barebones layout styled with a `globals.css`

Since we wanted to mimic Astro's approach to prefetching, Next.js’s `getStaticProps` and `getStaticPaths` were core here. Given that we had 10K blog posts, we opted for prerendering every page and post at build time.

```javascript
export async function getStaticPaths() {
  const posts = await getAllPosts();
  const paths = posts.map((post) => ({ params: { slug: post.slug } }));
```

Prerendering 10K+ pages was a breeze. While the build time increased, the result was a blazing-fast static site. The fully prerendered pages made page loads almost instantaneous.

I found Next the "easiest" to work with given that it's incredibly well documented, and being the fave of the React community, the available resources to getting started were the easiest. I found it to be the most "common sense" approach to building a website, but that's already biased since I've mainly worked with React in the past.

### With Nuxt.js

We kicked off the Nuxt project using the `npx nuxi init` setup. We opted out of TypeScript and Tailwind for the sake of simplicity and to match our “no CSS framework” rule across all projects.

Nuxt’s core dependencies were minimal and straightforward, but it did include a few Vue-specific packages like `vue-datocms` for rendering structured text. Here’s the lean list we ended up with:

```plaintext
├── @nuxtjs/eslint-config@9.0.0
├── dotenv@16.4.5
├── vue-datocms@1.0.0
├── nuxt@3.14.0
└── graphql-request@5.2.0
```

The folder structure was minimal, similar to our Next.js project. We used a standard `/pages` directory for routing, with `/pages/blog/index.vue` listing out all the posts and `/pages/blog/[slug].vue` for individual posts. The routing was straightforward with Vue’s `NuxtLink`, and everything played nicely with minimal boilerplate.

What I liked here with Nuxt was the automatic data fetching capabilities with `useAsyncData`. Given it was the first time I'd used it, we didn’t need to set up custom server-side data fetching hooks or deal with any complexity. Instead, we just fetched data directly in the page components using `useAsyncData`, keeping the code clean.

Nuxt’s handling of `useRuntimeConfig` also made it really easy to manage the DatoCMS API.

Since we wanted to prerender everything at build time though, there were some quirks Nitro, especially when attempting to pre-generate all 10K+ post pages. However this was a super simple configuration using generateRoutes to get all posts from the DatoCMS API by creating a script:

```javascript
import { getAllPosts } from '../lib/datocms.js';

export async function generateRoutes() {
  const posts = await getAllPosts();
  return posts.map(post => `/blog/${post.slug}`);
}
```

And having hooks run this async in the `nuxt.config.ts`

```typescript
hooks: {
    async 'nitro:config'(nitroConfig) {
      const routes = await generateRoutes();
      nitroConfig.prerender = nitroConfig.prerender || {};
      nitroConfig.prerender.routes = ['/', '/blog', ...routes];
    },
  },
```

In the end, Nuxt’s DX was also really nice. It took me some getting used to with the whole template-before-script flip around script, but the framework was still fun to work with.

### With Astro

Astro might have been my fave to work with in this even though I was more familiar with Next. Known for its “content-focused” approach and "0 JS" approach, I tweaked the comparison to put all other frameworks head to head with its' pre-render everything direction. I felt it was the perfect candidate for a site like this with 10K blog posts. We started the project with `npm create astro@latest` and went with the default choices, skipping any CSS framework.

Astro’s dependency list was slim and to the point, relying heavily on Astro’s built-in features:

```plaintext
├── @astrojs/node@1.0.0
├── dotenv@16.4.5
├── @datocms/astro@1.1.0
├── graphql-request@5.2.0
└── astro@3.5.0
```

We used Astro components for the core structure and pulled in React for structured text rendering with `@datocms/astro`.

Astro’s routing was the simplest of all for me. We just placed our pages in `/src/pages` with `/blog.astro` for the index and `/blog/[slug].astro` for individual posts. Astro’s DX was exceptional, with clear error messages and fantastic documentation.

In many ways it felt like going back to Next's src structure before app-router, back when Next felt simpler and more lightweight with a focus on websites.

Astro’s default mode is to prerender everything. While this made the setup easy, it also resulted in very long build times by default when generating all posts. Astro’s strength is supposed to lie in its focus on performance and content rendering. For content-heavy websites, Astro’s approach of fully pre-rendering everything makes it the perfect choice if you want a fast, SEO-friendly site without any client-side JS bloat.

### With Svelte

SvelteKit was the most unfamiliar territory for me, but I'd been looking for an excuse to try Svelte for a while. Following a `npm create svelte@latest`, I opted for Svelte 5, which at the time was 2-3 days freshly released.

Similar to all, the core deps were minimal. Svelte’s compiler does most of the heavy lifting, so we didn't need a lot to get started:

```plaintext
├── @sveltejs/adapter-node@5.2.8
├── dotenv@16.4.5
├── graphql-request@5.2.0
├── svelte@5.0.0
├── svelte-preprocess@5.1.0
└── svelte-kit@1.0.0
```

Svelte’s file-based routing was reminded me of Next in many ways, but getting used to typing `+` all the time took some getting used to. We placed our pages under `src/routes`, with `/blog/+page.svelte` for the blog index and `/blog/[slug]/+page.svelte` for individual posts.

Data fetching in SvelteKit is usually done with `load` functions in `+page.server.js` files. This made server-side data fetching using `graphql-request` intuitive.

Prerendering with SvelteKit was straightforward but required some more configuration. We used `adapter-node` to deploy our app and enabled full prerendering via `sveltePreprocess` in `svelte.config.mjs`.

```javascript
import { sveltePreprocess } from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-auto';

export default {
  preprocess: sveltePreprocess(),
  kit: {
    adapter: adapter(),
    alias: {
      $lib: './src/lib',
    },
  },
};
```

This allowed us to generate all blog post pages at build time. SvelteKit’s build process was incredibly fast, even with this massive amount of content.

SvelteKit’s DX was the most unique. It felt more “bare metal” compared to the other frameworks, and once I got a bit more familiarised with the syntax, it felt more "natural language-y" to type.

## The deploying and compiling

I opted to deploy all of them on Vercel since it was what I was the most familiar with - and as far as I could see, CF pages needed some extra config to work with some frameworks that I wasn't too keen on setting up at the moment.

With consistent deployment experience across frameworks, I just went with it.

While I played around with several deployments here and there, here's an "average" I found for each project when **not** pre-generating all paths at build time .

| Framework | First Build | Build with cache |
| --- | --- | --- |
| Astro | 31s | ~12s |
| Next.js | 47s | ~18s |
| Nuxt | 43s | ~15s |
| Svelte | 39s | ~14s |

Vs. a comparison when pre-generating all paths at build time for just 1,250 posts.

| Framework | Av. Fully Prerendered Build Time |
| --- | --- |
| Astro | 54s |
| Next.js | 72s |
| Nuxt | 67s |
| Svelte | 58s |

At 1K posts, each framework barely had anything above one another, so we thought, why not stress test it a bit more, and redo this experiment with 10K posts.

That's where *some* interesting insights came out. Here's the comparison on the initial build times for 10K posts,

| Framework | Initial Build Time |
| --- | --- |
| Astro | 5m42s |
| Next.js | 4m37s |
| Nuxt | 4m59s |
| Svelte | 4m23s |

vs. the build times averaged out once we had caches in place.

| Framework | Av. build time with cache |
| --- | --- |
| Astro | 4m44s |
| Next.js | 3m28s |
| Nuxt | 4m3s |
| Svelte | 3m51s |

This is where I learned about rendering engines 😅 Astro uses Vite’s SSR capabilities, which can be slower for really large-scale static generation because it re-parses and compiles every `.astro` file. Next.js is built on top webpack which is more efficient in handling large numbers of pre-generated pages.

Behind the scenes, the dependencies and overall bundle size impacted the build output too of course, and here too, with it's minimal approach, Astro really shined though.

| Framework | Core Dependencies | Build Output |
| --- | --- | --- |
| Astro | 4 | 4.2MB |
| Next.js | 9 | 6.8MB |
| Nuxt | 5 | 5.5MB |
| Svelte | 9 | 4.8MB |

And finally, on the client side, the same trend can be seen with the overall load on the browser. Astro's zero-JS approach makes it the lightest on the frontend, with Next remaining as the largest.

| Framework | JS Bundle | Initial JS |
| --- | --- | --- |
| Astro | 0KB | 0KB |
| Next.js | 120KB | 78KB |
| Nuxt | 90KB | 65KB |
| Svelte | 25KB | 15KB |

## The performance roundup

**And finally, everyone's fave flex metric - the Lighthouse.**

We ran the Lighthouse on each page type of each project to compare them head to head - keeping in mind that we haven't done any fancy custom OG customizations or anything beyond a sitewide default.

Another small note - they'd all most likely be "100" if I spent more time optimizing assets and minimizing some JS, but without any of those best practices added in, this is what I ended up with:

| Framework | Performance | Accessibility | Best Practices | SEO |
| --- | --- | --- | --- | --- |
| Astro | 99 | 100 | 100 | 100 |
| Next.js | 95 | 100 | 100 | 100 |
| Nuxt | 96 | 100 | 100 | 100 |
| Svelte | 97 | 100 | 100 | 100 |

So, in the end, **Astro won** it for me for pure content sites given the following:

-   Zero JavaScript by default meant fastest loading times and best Lighthouse scores giving me better performance.
-   Minimal configuration and templating gave me the "simplest" DX.
    
-   I had consistent build times.
-   Considering it needed minimal dependencies and smallest client-side resource usage, it also had the smallest footprint.
    

All things considered, I consider Astro as my winner. Blowing up the scope of the project did start skewing the performance metrics back in favour of Next, but Astro felt really fun to work with, and if a site isn't tremendously massive and/or not content focused, I think Astro can be a really great choice.

Though does any of this matter? Don't listen to me, keep using whatever you're most comfortable with 😅

PS: Shoutout to Silvano, Marco, and Marcelo from the team for babysitting me through Vue, Svelte, and the CMA 🫶🏽

---

# DatoCMS is officially ISO 27001 certified

Source [blog]: https://www.datocms.com/blog/iso-27001.md

Posted on [date: 2024-11-26T09:14:22.292+01:00] by Matteo Giaccone

We've got some news to share.

**DatoCMS is officially ISO 27001 certified**! This is a big milestone for us, and we’re here to let you in on what it means for us—and more importantly, for you.

### So what's ISO 27001?

In simple terms, ISO 27001 is an international standard for managing IT security. Think of it as a rigorous checklist to prove that we’ve got our act together when it comes to protecting your data. It covers everything from how we secure our systems, APIs, and tooling, to how we train one another to keep things secure on an ongoing basis.

In short? It’s our way of saying, "Your data’s safe with us, and we’re not messing around."

We went through this rigorous process as part of our commitment to maintaining the [highest levels of data security and protection](https://www.datocms.com/legal.md) for our users.

[Request the full reports](https://datocms.com/contact)

### What this means for you

Trusting software with your data is a big deal—whether you're building an enterprise-level site or a personal blog. We've always been very sensitive about this topic, from where we host data (🇪🇺), to what third party processors we use, and so on.

With ISO 27001, we’re not just telling you we’re secure; we’ve had a third-party expert come in and check that everything is rock-solid.

This means:

-   **Better security**: We’ve got systems, processes, and guardrails in place to keep your data safe on an ongoing bases.
-   **Peace of mind**: You can focus on building awesome projects while we handle the security stuff.
    
-   **Enterprise-friendly**: For those of you working with strict compliance requirements on software choices, this certification checks all the boxes.
    

Getting certified for this isn't just a pay-to-play “sign a form and call it a day” kind of thing. It was a months-long process involving every part of our team. We evaluated risks, implemented new controls, trained our crew, and put all the pieces in place to meet the rigorous standards, so we're committed to ensuring your content and data have the best possible security.

If you’re curious about what this means or how it impacts you in more detail, feel free to reach out. Alternatively, if you're keen on diving into more details on the technicalities and coverage of this certification, check out the [Wiki entry](https://en.wikipedia.org/wiki/ISO/IEC_27001) or the [official website](https://www.iso.org/standard/27001).

---

# Personalization with a Headless CMS

Source [blog]: https://www.datocms.com/blog/headless-cms-personalization.md

Posted on [date: 2024-12-09T17:40:11.246+01:00] by Ronak Ganatra

### TLDR

-   There's 4 approaches to implementing personalization, each with varying levels of integration and dependability on your CMS:
    
    -   Client-side: This happens directly in your user’s browser, and has almost no influence from your CMS.
        
    -   Server-side: This happens on your side before webpages are loaded for users, and ideally should involve variations being queried from your CMS.
        
    -   Branch-testing: Branch testing can be "script-free", and therefore the best performant way to test things, but requires a lot of technical knowhow and would have heavy dependence on your CMS.
        
    -   API-first: This involves integrating your CMS with personalization engines where everything is dynamically run based on your requirements. It requires a heavy investment in setting things up correctly and is very dependent on integrating with your CMS to query content.
        
-   There's no "right way", as always the answer is "it depends" - however the correct-er approach is based on your use-case and traffic rather than discomfort with complexity.
-   Some considerations when selecting the right approach involve impact on SEO, technical resources at hand, sophistication of your stack, "needs" based on traffic rather than just "wants", and, of course, how much you're willing to spend.
    

---

Devs are usually pretty comfortable setting up a Headless CMS for all sorts of projects, but as soon as the project starts to involve the marketing team, we notice a few really common topics pop up.

Many times, that topic is, "Cool, but with our current CMS I use X for AB Testing, can I still use that?"

Now, for the sake of simplicity, let's sacrifice the logic behind differentiating between AB Testing, Personalization, Segmentation, Experience Optimization, ..., and Individualization. We marketers love our 50 shades of buzzwords, so this time around, let's just incorrectly club them all together under personalization (something we're definitely not guilty of having ever done before 😶‍🌫️).

With all the super sophisticated online things we run today, from news with ads to ecommerce and in-game marketplaces, personalization is no longer a luxury—it’s a necessity. At least above a certain monthly traffic threshold.

Users expect some form of subliminal familiarity that caters to their preferences, behaviors, and needs. But if you’re using a Headless CMS, weaving personalization into your stack can seem intimidating, and ngl, it often can be.

(Video content)

So, let's dive into four distinct approaches—client-side, server-side, branch testing, and API-first solutions—so you can pick the one that aligns with your project’s goals and tech stack the best. We'll also throw in some tools to check out, whether or not we partner with them.

*Note: While many tools would handle multiple approaches, we'll only recommend them in the sections where we've tried them out. This isn't a "top 10 personalization tools" kinda post, for that* [*there's G2*](https://www.g2.com/categories/personalization)*.*

### So. What're we dealing with?

We've evolved faaaar beyond the days of A/B testing CTA colors and labels, and entered a chaotic phase of “Experience Optimization,” that spans across everything from dynamic landing page variations to hyper-individualized customer journeys across channels and platforms.

Today’s solutions are robust, making real-time decisions and integrating seamlessly with CDPs, Analytics, CMSs, and other DXPs to craft insanely optimized experiences for every individual interacting with your digital product. In theory. That's still a LOT of work to set up.

Performance on the other hand, of course, is the elephant in the room. Whether it’s client-side, server-side, on-the-edge, branch testing, or real-time segmentation, these terms have shifted from being just buzzwords to buzzwords that have very real consequences. And for good reason—[every millisecond of delay can hurt revenue](https://www.gigaspaces.com/blog/amazon-found-every-100ms-of-latency-cost-them-1-in-sales) in ways you’ll feel, especially if you're in high-volume eCommerce. So how do we go about getting personalization right without tanking performance?

To strike the right balance, there are two big questions to tackle.

First, what level of personalization do you actually *need* (not just want) and why?

That's something for you to figure out, we can't help you there.

And second, how do we go about achieving that?

Let's talk about that.

### Breaking down different approaches

Let's compare the 4 approaches, as well as look into how they work with the CMS, and what considerations you need to have when selecting one. We'll also throw in some suuuuuuper oversimplified diagrams to help visualize which parts are doing the heavy lifting in each.

#### Client Side

Client-side personalization happens directly in your user’s browser. When a user visits your site, scripts you've added into someplace like a tag manager would dynamically fetch and display content tailored specifically for them.

(Image content)

If you've ever been to a Benihana type of restaurant concept, think of it as your chef preparing the meal right at your table, crafting the experience in real-time based on the user’s preferences. How elaborate or show-ey the chef is dictates how long before you get your food.

(Image content)

The same principles apply here. Depending on *how much* you're asking the browser to load would impact how long a user has to wait for their personalized content.

**What's good?**

This approach is incredibly flexible. Developers can easily tweak, test, and iterate on personalization strategies without requiring backend redeployments. In fact, depending on your setup, you might not even need any developer interference at all (pour one out for Google Optimize 🫗). By offloading the work to the browser, it also reduces the strain on your servers. The speed of experimentation is another major advantage—you can implement changes quickly and refine the experience in real-time, often with frontend editors to let you see *exactly* what changes you're making.

**What's not so good?**

The biggest drawback of client-side personalization is its impact on SEO and performance. Since content is rendered dynamically in the browser, search engine crawlers may not see the final version, potentially hurting discoverability. This can be circumvent by each experiment having a unique URL, slug, or URL parameter, but there's a whole other can of worms with that. There’s also the risk of latency, as loading personalization scripts can slow down your page load time. Additionally, it relies heavily on JavaScript, so users with JS disabled may miss out entirely. Script and Ad blockers would also play a role here, with many browsers just not loading the scripts at all.

**Note on integration with a Headless CMS**

None at all really. Everything's handled in a frontend editor on the platform, so you don't need it to integrate with your CMS at all.

**Recommendations**

Collectively the legit ones we've tried out are [VWO](https://vwo.com/), [AB Tasty](https://www.abtasty.com/) and [Unbounce](https://unbounce.com/). You wouldn't need any integration to the CMS, since you'd edit stuff on their frontend once you implement their scripts in your website repo or tag manager.

#### Server Side

Server-side personalization does the heavy lifting before the user even loads your page, putting the strain on you.

(Image content)

The server tailors content to the user’s preferences and delivers it ready-to-go. To stick with our restaurant analogy, it’s like a waiter presenting you with your burger, prepared to perfection behind the scenes. The kicker being they remembered not to add tomatoes and to put in extra pickles, because they already know your preferences.

**What's good?**

Server-side personalization is highly SEO-friendly, as the content is generated on the server and can be crawled and indexed by search engines since it's not being built on the fly at load time. It also boosts performance by sending pre-tailored content to the browser, reducing load times for users (no flicker). Security is another plus, as all personalization logic stays on the server, safeguarding sensitive data and logic from client-side exposure.

**What's not so good?**

This approach adds strain to your infrastructure, as it handles the extra work of generating personalized content. It also tends to be more complex, requiring stronger backend systems than just a shared hosting, and potentially slows down development cycles since you might need developer support depending on how complicated your experiments are.

**Note on integration with a Headless CMS**

A good approach is making sure your variations are querying for different content records from the CMS, so this approach had a medium-to-heavy dependence on integrating with your CMS, even if you set experiments in the tool itself.

**Recommendations**

Again, [VWO](https://vwo.com/) and [Optimizely](https://www.optimizely.com/) are the "popular" ones we've tried here that worked really well. For the more technical ones, [Uniform](https://www.uniform.dev/) integrates with many Headless CMS, and Vercel offers [Vercel Middleware](https://vercel.com/resources/edge-middleware-experiments-personalization-performance) to leverage edge functions to personalize content based on location, cookies, or other dynamic inputs.

#### Branch Testing

Branch testing involves creating separate versions—or branches—of your site to test different personalized experiences. Each branch is deployed independently, allowing teams to experiment with significant structural changes.

(Image content)

To exhaust the same analogy, it's like taking your partner to a fancy restaurant where there's no a-la-carte or buffet, but just 2 set menus to choose from (or more, depending on how many branches you want to run in parallel).

**What's good?**

Branch testing is perfect for major structural experiments or testing new content strategies. By isolating branches, you can make changes without impacting the main site, ensuring stability. This approach also allows for scalable experimentation, as branches can be developed, deployed, and analyzed independently. It is a highly technical approach, and prone to errors, so it would definitely require considerable technical input from devs, but it also tends to be the most performant option.

Looking for terrible life advice on bad strategy? If you've got too many tests running on a domain and it's slowing everything down, splitting your site into `x` branches let's you multiply your tests by `x`.

**What's not so good?**

This is where you can forget that terrible advice, because your devs will probably shoot it down anyways 🤭

Setting up branch testing requires CI/CD pipelines, which can and will slow down iteration cycles since the implementation requires changes to your website's codebase, and deploying multiple branches in parallel which most certainly will go out of sync with your main website, meaning it will constantly need to be rebase for *other* parts of the website to stay up to date with the experiments.

It also comes with the overhead of maintaining these branches, which, depending on your setup, might exponentially overcomplicate errors, tests, and other safety checks, making it resource-intensive for teams without strong development workflows.

**Note on integration with a Headless CMS**

This is completely dependent on your CMS. Since each branch would query variations from the CMS, you need to make sure that your website repo is strongly integrated and all the relevant content types or models needed are queried correctly.

**Recommendations**

For branch testing we've tried out [Netlify Split Testing](https://docs.netlify.com/site-deploys/split-testing/) which integrates branch management into your CI/CD pipeline. While Vercel's edge middleware approach is the right one, you can also take their preview deployments approach to have dynamic previews for each branch, mimicking what Netlify offers.

#### API-First

API-first personalization, as the name implies, uses APIs to fetch and deliver tailored content dynamically. So, for one last time, let's get back to our overused analogy. This approach is like hiring a personal chef to craft a unique menu for you, and just for you, based entirely on your preferences and data. It’s all about offering a bespoke experience, no matter the complexity. You get what you want.

(Image content)

Given Headless CMS are just a "hosted API" for your content, this approach is also the more "warm and fuzzy" one for your stack, since everything is compatibly handled with the best performance and flexibility in mind, and there's no limitations or restrictions on what vendors or software you can use.

**What's good?**

API-first personalization is incredibly flexible. Like, individual-user-level flexible. John can get something completely different from Jane, even if they're both 99.9999999% similar as people.

This allows you to highly customized experiences for even the most complex needs. Its scalability is unmatched, enabling seamless integration across channels and devices. Additionally, it’s a future-proof solution, as APIs can adapt to changes in your stack without major rework.

Ok, I'll stop hyping it like a fan-boy now.

**What's not so good?**

As expected, this approach demands mad skills and robust infrastructure, making it less accessible for smaller teams or simpler projects. These tools also tend to be pretty expensive since they require a very tight relationship to your stack and CMS. And, since the possibilities are endless, it’s also really easy to over-engineer this setup because you really will feel like a kid in a candy shop looking at your options, which often leads to adding unnecessary complexity to your setup.

**Note on integration with a Headless CMS**

This is also completely dependent on your CMS. These tools simply require variables to query certain content types from the API of your CMS, and tend to be intelligent enough to put together the right content for experiments provided the setup is done well.

**Recommendations**

For API-first personalization, [Dynamic Yield](https://www.dynamicyield.com/) integrates nicely with all Headless CMS (and they're one of [our official partners](https://www.datocms.com/tech-partners/dynamic-yield.md) too).

Its' worth noting that most platforms in this space *tend* to be eCommerce focused, so that's where names like Nosto and Styla would also pop up.

### WWYD?

So there you have it.

Do you go with the speed/simplicity of client-side personalization, the SEO and security perks of server-side, the experimental approach of branch testing, or the enterprise-grade scalable, robust, AI powered, (insert a few more buzzwords) of API-first platforms?

While there's no wrong answer, there's definitely a "right-er for you" one. So if you're keen to see how your plans and CMS stack up together, or whether you want to shoot some ideas to find the right approach, [let's chat](https://datocms.com/contact).

---

# A look back at 2024

Source [blog]: https://www.datocms.com/blog/a-look-back-at-2024.md

Posted on [date: 2024-12-18T11:05:11.864+01:00] by Stefano Verna

As 2024 comes to a close, it's once again time to reflect on the past year. It’s been another packed year, and it’s great to look back at everything we achieved, day by day.

Want to take a walk down memory lane? Here are previous editions: [2023](https://www.datocms.com/blog/a-look-back-at-2023.md), [2022](https://www.datocms.com/blog/a-look-back-at-2022.md), [2021](https://www.datocms.com/blog/a-look-back-at-2021.md), [2020](https://www.datocms.com/blog/a-year-in-review.md).

Alright, let’s dive into the highlights and celebrate how far we’ve come! ✨

---

## Breaking records: €6 million in revenue! 🌟

This year, we hit an incredible milestone: €6 million in revenue! That’s a solid **30% year-over-year growth**, a testament to the steady momentum we’ve built over time. Ten years ago, when the first lines of DatoCMS code were written, this achievement would have seemed simply unimaginable.

(Image content)

But beyond the numbers, what fills us with pride is how we've reached this point — maintaining our independence and staying focused on what truly matters for us and our customers, ignoring trends, vanity, and grandiose ambitions. What’s remarkable isn’t just the number — it’s the context. The headless CMS space has matured, moving past the hype into a phase of grounded utility. **And yet, here we are, thriving.** This proves one thing: we’re not just surviving — we’re here to stay.

This year, with our usual composure and without too much fanfare, DatoCMS powered **~2 billion API calls per month** (+60%), managed a total of **7PB of asset traffic** (+30%), and delivered **4M hours of video streaming** (+181%).

(Image content)

We also managed **+2,000 support requests**, resolving over 60% of them within 8 hours — and that includes free users.

These figures really are a testament to what’s possible with a small, humble team of laser-focused individuals — dispelling the myth that success demands sprawling teams and endless VC funding.

---

## Achievements and Highlights

### ISO 27001 Certification 🔒

In 2024, [we earned our ISO 27001 certification](https://www.datocms.com/blog/iso-27001.md), a badge of honor that underscores our commitment to security and reliability. It’s more than just a certificate; it’s a reflection of the rigorous standards we’ve implemented to safeguard customer data. Plus, it makes enterprise contracts a smoother ride for everyone involved.

### Expanding resources: Academy, User Guides, Glossary 📚

We have always believed in empowering all our users, not just developers. This year, Ronak introduced a ton of new resources such as the [Academy](https://www.datocms.com/academy.md), [User Guides](https://www.datocms.com/user-guides.md), and [Glossary](https://www.datocms.com/glossary.md) — featuring tens of hours of content aimed at making DatoCMS more accessible for content creators and editors.

(Image content)

My favourite content? [The lo-fi break videos!](https://www.datocms.com/user-guides/content-modeling/bonus-content-lo-fi-break-for-blocks.md) 😂 Jokes aside, we really hope these tools can help bridge the gap, ensuring that everyone can fully unlock the platform’s potential.

### Project Starters, reimagined 🛠️

Developers weren’t left out either. We reorganized and enhanced our [Project Starters](https://www.datocms.com/marketplace/starters.md), dividing them into:

-   **Starter Kits:** Minimal scaffolds for integrating DatoCMS with frameworks like [Next.js](https://www.datocms.com/marketplace/starters/next-js-starter-kit.md), [Astro](https://www.datocms.com/marketplace/starters/astro-starter-kit.md), [Nuxt](https://www.datocms.com/marketplace/starters/nuxt-starter-kit.md), and [SvelteKit](https://www.datocms.com/marketplace/starters/sveltekit-starter-kit.md) and kickstart your custom website right away.
-   **Full-Fledged Starters:** pre-built [Ecommerce](https://www.datocms.com/marketplace/starters/ecommerce-website.md) and [Marketing Website](https://www.datocms.com/marketplace/starters/marketing-website.md) projects to see all of DatoCMS's features in a realistic production-ready setup, with many example content types, and advanced features.
    

This clear distinction in purpose for different people's needs is already helping a lot, and the ongoing effort of always keeping these repos updated has been dramatically simplified.

### A better product discovery experience ✨

This year marked the launch of [try.datocms.com](http://try.datocms.com/) — the best way to explore DatoCMS without registration. This interactive experience includes the full product, plus video tutorials, and a live demo website that instantly reacts to content changes. The average boot time is below 10 seconds.

Since its launch 5 months ago, it has already been used **more than 3,000 times**, and we hope it continues to allow users to quickly grasp the capabilities of our platform, removing barriers and making discovery effortless.

### Revamping our website: from Next.js to Astro 🚀

In the last quarter of 2024, we also managed to completely rewrite our website, transitioning from Next.js to Astro. The result? A faster, more streamlined site that better embodies our vision of what a good content-driven website should look like.

Throughout this process, we've discovered a ton valuable insights that we've decided to share on our blog for others to benefit from. The ongoing series starts from this [first article](https://www.datocms.com/blog/why-we-switched-to-astro.md)!

### Focusing on you 🫵

Of course, the highlight that hits closest to our hearts is the incredible work that you all have been doing — our users, customers, and partners.

In the last twelve months, our official [**Agency Partner Network**](https://www.datocms.com/partners.md) **literally doubled** from 82 to 158 — an achievement that reflects the daily support we provide on both sales and technical fronts, always delivered with the human touch we value so much.

Also the number of showcased projects more than doubled, prompting us to launch the [**Agency Projects Showcase**](https://www.datocms.com/partners/showcase.md): a dedicated page gathering the stunning projects agencies are creating with DatoCMS. From ecommerce to gaming, fintech to entertainment, luxury, tourism, fashion, sports, and beyond — there’s no industry untouched by their creativity and technical expertise.

This growing momentum also inspired Ronak and Matteo Papadopoulos to set up some [casual chats that highlight the work put into these projects](https://www.datocms.com/customer-stories.md) beyond just the "generic case study stats". These friendly conversations bring together agencies, customers, or both, to share ideas, challenges, and successes.

(Image content)

Thank you Jökull, Jörg, Derrick, Gerald and Emil! ❤️

It's all about fostering connections, learning, and showcasing what makes the headless stack a new welcome standard. And from [dreipol's 3D texture generation](https://www.datocms.com/casual-chats/dreipol-sfg-basel.md) to [Trip To Japan's personal travel itinerary builder,](https://www.datocms.com/casual-chats/trip-to-japan.md) we continue to be amazed by the creativity in building stunning frontends.

---

## Game-changing features 🌟

This year, we delivered many major features we’ve long envisioned, alongside countless smaller enhancements that elevate every aspect of our platform.

It is also important to emphasize that our user research initiative, led by Matteo Balocco, played a pivotal role in organically identifying user needs and validating potential solutions — a challenging but essential process. Insights from this research have directly influenced several key areas, including discoverability, reuse, and content organization, which were central to our recent development cycles.

Here are some of the standout innovations developed by our (it's funny to think about it) 3½-person dev team:

-   [**Schema Interface Makeover**](https://www.datocms.com/blog/the-schema-interface-gets-a-makeover.md)**:** Our content modeling interface got a fresh redesign with new features like drag-and-drop organization, quick search, and emoji icons — making schema management more intuitive than ever.
-   [**Expanded Modular Content**](https://www.datocms.com/blog/expanding-modular-content-with-single-block-and-frameless-mode.md)**:** We introduced Single Block and Frameless modes, giving new life to modular content. Single Block simplifies content management by allowing specific block limitations, while Frameless mode enables seamless field sharing across models — a game-changer for consistent content structure.
    
-   [**Media Optimization Made Easy**](https://www.datocms.com/blog/making-media-optimization-a-breeze-with-datocms.md)**:** We improved media handling with automatic image optimization and new video components. Images now automatically compress and optimize using our DatoCMS Preset (achieving perfect Lighthouse scores!), while new framework-specific video components make it effortless to integrate Mux-powered streaming.
-   **Improved Video Management:** We enhanced our video capabilities with support for [4K streaming](https://www.datocms.com/product-updates/stream-videos-in-4k.md) (available for Enterprise plans), improved subtitle management including [auto-generated captions](https://www.datocms.com/product-updates/adjust-auto-generated-captions.md) with editing capabilities, and support for [SRT/VTT subtitle uploads and alternate audio track](https://www.datocms.com/product-updates/add-subtitles-and-audio-tracks-to-your-videos.md) support in various formats.
    
-   [**Data Usage Dashboard**](https://www.datocms.com/blog/monitor-your-data-usage-from-the-dashboard.md)**:** We introduced comprehensive usage monitoring and forecasting tools, allowing users to track bandwidth, API calls, and video streaming across projects. The new dashboard provides detailed analytics, usage predictions, and alerts to help manage resources effectively and prevent overages.
-   [**Cache Tags**](https://www.datocms.com/blog/introducing-datocms-cache-tags.md)**:** A groundbreaking feature that enables surgical page regeneration on content changes. This sophisticated caching system automatically manages invalidation, dramatically improves performance, and reduces hosting costs by ensuring only affected pages are regenerated. With zero configuration required, projects of any size can now leverage enterprise-grade caching strategies that previously required complex implementations.
    
-   [**Asset Collections**](https://www.datocms.com/product-updates/introducing-asset-collections.md)**:** A major enhancement to media management that introduces folder-like collections and sub-collections in the media area. Assets can be organized through drag-and-drop or bulk actions, making it easier than ever to maintain a structured media library. Collections behave like traditional folders, allowing for nested organization while ensuring each asset belongs to a single collection for clear categorization.
    

...but that's just the tip of the iceberg. As every year, we have worked on a mountain of small improvements and adjustments, such as [better management of webhooks](https://www.datocms.com/product-updates/improvements-to-handling-webhooks.md), a [new SDK for Astro](https://github.com/datocms/astro-datocms), [new hooks for DatoCMS plugins](https://www.datocms.com/product-updates/new-plugin-hooks.md), an [improved Structured Text editor](https://www.datocms.com/product-updates/enhancements-to-structured-text.md), [increased security for the GraphQL API](https://www.datocms.com/product-updates/improved-gql-visibility-control.md), [bulk actions for Modular Content](https://www.datocms.com/product-updates/bulk-actions-for-modular-content.md)... the list can go on and on. 😅

---

## Team dynamics and growth 🌱

I'm thrilled to announce that Luca Bonfiglio, a long-time friend, has joined our team to oversee financial operations. His expertise is already making a significant impact, enhancing both our processes and long-term strategy.

This year also marks a notable milestone: for the first time in a decade, a few team members have decided to move on to pursue other opportunities — or, in some cases, the decision was mutual. While departures are never easy, they are a natural part of any organization's evolution. I see this as a healthy indicator of growth and change.

After carefully evaluating our options and considering new hires, **we’ve decided to maintain our current team size**. This decision reflects our belief that we can achieve great things with the talented people already on board. It’s also a reaffirmation of our core philosophy: expanding the team only when absolutely essential.

I’d also like to give special recognition to [Marco Mezzavilla](https://www.marcomezzavilla.com/), who has been instrumental as an external contractor in developing our marketing website during 2024. His contributions have been very appreciated, and we’re definitely starting to consider him a part of the team!

---

## Looking ahead: Strategic infrastructure evolution 🔧

**Our total infrastructure costs for 2024 amount to approximately €850k**, allocated between CDNs, server expenses, billing/transaction costs, and other minor suppliers.

(Image content)

The most substantial item is surely related to the various CDNs we use to provide our service (Cloudflare, Fastly, Imgix, Mux). We are actively working to create competition among the (few) available players and find optimizations. According to our estimates, in 2025 we should be able to **reduce CDN costs by about €130k, a 30% decrease.**

As for the server-related costs, from the very beginning, we strategically chose Heroku to outsource our infrastructure management. This allowed us to focus entirely on our product during the years of most uncertainty. While the decision has been incredibly beneficial, we think it's time to make a change.

Currently, our annual server costs for Heroku amount to about €200k, with an annual increase of at least 8%, thanks to the grotesque contracts Salesforce now requires enterprise clients to sign. With Heroku’s stagnation over the last decade, lack of innovation, poor support despite our enterprise contract, and their lack of transparency during downtimes, we feel they've really pushed their luck.

While switching to alternative PaaS providers would surely offer better pricing, during this year we followed with much interest [Basecamp's complete exit from the cloud](https://world.hey.com/dhh/we-have-left-the-cloud-251760fb). The [reasons underlying this choice](https://world.hey.com/dhh/five-values-guiding-our-cloud-exit-638add47) surely resonated a lot with us: a stable SaaS business of our size should own their infrastructure, rather than continue renting it.

Initial estimates suggest that moving to physical servers would reduce our server and database costs to around €50,000 annually — an **80% savings in the short term, that would only grow over time.** But the benefits don’t end there. This shift would give us the freedom to tailor our architecture to our specific needs, improving performance and reliability.

Additionally, the significant cost savings would allow us to revisit our pricing model, ultimately **offering our customers more flexible and cost-effective options**.

We’ve already begun preliminary experiments. We know this is a complex decision, but we also believe this is one of our top priorities for 2025, and we are committed to approaching it with the utmost seriousness and care.

---

## Thank you! 🙏

As we close out 2024, we want to express our heartfelt gratitude to our customers, team members, and community. Your support and collaboration inspire us to keep striving for excellence, year after year. You are the reason we do what we do, and we couldn’t have achieved these milestones without you.

Here’s to a successful 2025 filled with innovation, growth, and continued collaboration.

Happy holidays, and we look forward to seeing you next year! 🎄

---

# Le Pain Quotidien

Source [case-studies]: https://www.datocms.com/case-studies/le-pain-quotidien.md

## 200+ locations. 20+ countries. 55+ locales. Single source of truth.

Learn how November Five built Le Pain Quotidien a state-of-the-art new digital identity for their global brand.

(Image content)

## At a glance...

### 200+

Locations in 20+ countries

### 55+

Locales handled from a single project

### 33+

Hours to slow-ferment bread. This took... longer.

### The challenge

Le Pain Quotidien needed a new digital presence for their global content in 55+ locales across 200 franchised locations in 20 countries.

### The result

November Five built them a gorgeous and scalable digital storefront from the ground up focusing on their core values of storytelling, using DatoCMS and Next.js.

[Le Pain Quotidien](https://lepainquotidien.com/) is a bakery-restaurant brand with more than 200 locations across 20 countries. Known for its communal tables, slow-fermentation breads, and seasonal menu, the brand has cultivated a distinct identity grounded in storytelling and craft. But until recently, that story had a few technical limitations coming in the way of being conveyed and doing it full justice.

Limitations that the fine folks over at [November Five](https://novemberfive.co/) took care of.

So, to dive into what they got up to, we sat down with November Five's very own Stijn Symons (their Director of Architecture), and Vincent Pauwels (their Co-Founder).

Le Pain Quotidien had grown fast over the years. However, on the technical side, there was no reliable way to scale content consistently without duplication. The global content team was small, but the surface area they had to manage kept growing. Over time, quality drifted. Stories existed in some markets but not others. The web presence no longer reflected the care and consistency of the in‑store experience.

The challenge was not just to rebuild a website. It was to turn a fragmented digital estate into a single global platform that could carry their story consistently, while still allowing local teams to adapt content where it truly mattered.

But Le Pain Quotidien isn't just a chain, it's always been a story. And that story mattered. So November Five needed to make sure that everything about this project put storytelling at the center to balance out the brand's essence with as much importance as the technical side of things.

PS: For more insights into the process of building out this narrative‑first digital storefront for Le Pain Quotidien, head over to [November Five's website](https://www.novemberfive.co/cases/le-pain-quotidien-website) →

### The Human Layer

Before a single schema was touched, the team flew over to spend a day at the bakery.

No, Seriously. When we spoke, Stijn and Vincent and we were really vibing on the time, patience, and love that goes into the bread-making process.

The folks at November Five spent time at the atelier, watched how the bread was made, talked to staff, and took notes. Lots of notes. They saw what 33 hours of fermentation looked like. They asked questions about the communal table, about the history, about the recipes, and so much more. They tried to understand not just how Le Pain Quotidien operated, but what they believed.

That shaped everything that came after. It’s why the site doesn’t start with a promo or product grid. It opens with a poem and a loaf of bread. It's why there's no generic about page. There's an insight into the [Atelier](https://www.lepainquotidien.com/es/en/atelier).

(Video content)

Its the little things.

> We thought we needed an About page. After a day in the bakery, we said nope. What we need is an Atelier.

It’s also why the word "storefront" stuck. This wasn’t just a set of pages. It was an extension of the in-restaurant experience. It had to feel slow, warm, and thoughtful. Not transactional.

Even internal buy-in reflected that. Stakeholders weren’t shown mockups. They were shown meaning. And when they saw the first version of the homepage, a stripped-back space with no CTA, no menu button, just bread and philosophy, they got it.

Naturally, right after that, the panic set in.

> Everyone loved the vision. Then came the fear. How the hell are we going to maintain this across 25 countries?

How the hell, indeed...

### Bringing it all together

At the core of the solution is DatoCMS. Everything from the philosophy-driven [blocks](https://www.datocms.com/features/dynamic-layouts.md) to the [localizations](https://www.datocms.com/features/headless-cms-multi-language.md) and [assets](https://www.datocms.com/features/images-api.md) are structured and managed in a single project.

(Video content)

Instead of a classic multi-site setup, the team introduced *exception-based* [*content management*](https://www.datocms.com/features/schema-builder.md). The idea is great: global content defaults to language-level versions. Editors only create locale-specific overrides when absolutely necessary, to ensure that regional and local aspects can be introduced per-locale when needed.

(Video content)

For example, if a story is written in English and needs a French translation, DatoCMS handles that cleanly, but if a specific region (say, Belgium) needs a different intro or component, they can override that field or block directly to update content accurately for `BE-fr`. No cloning entire records. No rework.

It’s a smart way to scale. Most content changes are made once and roll out across dozens of markets. But when a specific dish or story needs a local nuance, the system supports it cleanly, without duplicating everything or creating messy editorial workarounds.

> The old way, a single menu update meant 55 versions of the same dish. Now they just override what they need and fall back everywhere else.

The result is a lean, flexible architecture that fits the team’s actual capacity. LPQ’s central editors can support 25+ countries without burnout or chaos. And they finally have a clear view of what content exists, where.

Every page on the site, the homepage, the Atelier, even seasonal stories, are built using modular blocks in DatoCMS.

These blocks are mapped directly to frontend components in [Next.js](https://www.datocms.com/docs/next-js.md). Editors get a [structured WYSIWYG experience](https://www.datocms.com/features/dynamic-layouts.md) that still honors design constraints. No broken layouts. No inline spaghetti.

It’s the best of both worlds. Editors can remix content, reuse components, and build rich storytelling layouts without writing code or relying on developers.

And because the blocks reflect real brand ideas, like “the fourth ingredient is time”, editors aren’t just filling forms. They’re shaping experiences.

The site itself runs on a Next.js frontend deployed globally via AWS. Pages are statically generated where possible, with ISR for dynamic content like store hours and seasonal dishes.

[DatoCMS’s GraphQL API](https://www.datocms.com/features/headless-cms-graphql.md) feeds exactly what’s needed per component, and nothing more. No bloat. No waste. Every interaction feels smooth and fast, even on image-heavy pages, and wow does this site have some great visuals sprinkled everywhere.

Speaking of images. this site is packed with hi-res images from all across the world. Le Pain Quotidien relies heavily on rich photography, but they didn’t want to hand-edit crops for every device.

(Video content)

No problem.

> Just tell the client they don’t need to use Photoshop anymore. The look on their face is wild. And honestly, we forget to even mention it now because it just works.

The site leans on [DatoCMS’s image API](https://www.datocms.com/features/images-api.md) for format conversion, smart crops, and responsive delivery.

### Moment of truth

Ok, so we've all loved talking about the technical nittygritties and details behind the scenes, but at the end, it doesn't really matter if the Le Pain Quotidien team struggles to keep the story going once they've been handed over the project. So the editorial experience REALLY needed to come through, and November Five did a stellar job of it.

(Video content)

The editorial team isn't huge. And they’re not developers. So everything had to be fast, safe, and most importantly, sane. And the feedback that the team shared with November Five reflects that.

Le Pain Quotidien uses [real-time previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md), [scheduled publishing](https://www.datocms.com/user-guides/content-management/content-records-publishing-scheduling-and-versioning.md), and locale-specific fieldsets to work without friction. [Permissions are finely tuned](https://www.datocms.com/user-guides/the-basics/intro-to-settings-configurations.md), so franchisees can edit what they need, without stepping on the global structure.

[AI-assisted translation](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md) helped onboard all 55 locales during the initial migration. Editors can copy content between languages and refine from there, not start from zero.

> It’s faster than anything we’ve used. And I love that I can just paste in a photo and not worry about it being the right size. It just... works.

### So. What's next?

Now that the foundation is solid, the team is starting to layer on more.

One idea in the works is onboarding menus to the website. Every dish is now modeled as structured content, linked to seasonal campaigns, photography, and editorial storytelling. The croque monsieur in Paris might look and taste a little different than the one in New York, and the CMS needs to support that.

But of course, a dish isn't just a dish when storytelling is at the heart of all content. Each dish can link out to a story about its origin or philosophy. It’s not just a sandwich. It’s a story entry point. And the team is already running experiments to test engagement and narrative flow to ensure that editors can do justice to each dish's story when its' time to publish.

Another idea in the works is a DatoCMS plugin to track content completeness across locales. Multi-market brands often struggle to keep everything up to date. This plugin would help editors and managers see where things are missing or inconsistent.

There’s also ongoing work to optimize internal navigation, surface story snippets more contextually, and keep improving the experience across channels.

> The best version of the site wasn't launch day. Its what's coming next!

Really exciting stuff going on!

What we loved so much about this project is that this wasn’t a typical site rebuild. It wasn’t about migrating content or adding features.

The tech stack didn’t get in the way of craft. It enabled it.

And now the story scales.

### Project

[lepainquotidien.com](https://lepainquotidien.com/)

### Use case

[Modern Websites](https://www.datocms.com/use-cases/modern-websites.md)

### Industry

-   Food & Beverage

### Key Features

-   [Editor experience](https://www.datocms.com/features/editor-experience.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Structured content CMS](https://www.datocms.com/features/structured-content-cms.md)
-   [Dynamic layouts](https://www.datocms.com/features/dynamic-layouts.md)
-   [Images API](https://www.datocms.com/features/images-api.md)

### Partner

[November Five](https://www.datocms.com/partners/november-five.md)

---

# Eurac Research

Source [case-studies]: https://www.datocms.com/case-studies/eurac-research.md

## 800~ Researchers. 20+ Websites. Dev team of 3. 1 CMS project.

How Eurac Research turned DatoCMS into the content and data hub for an entire research institution.

(Image content)

## At a glance...

### 100+

Editors create content in 3 languages.

### 5+

Custom plugins sync data across 10+ tools.

### 0

Chaos. Even with a small team of 3 developers.

### The challenge

Eurac Research needed a system flexible enough to serve 100+ editors across multiple institutes, sync with external research systems and tools, and handle multiple languages, all maintainable by a team of three.

### The result

DatoCMS became the content and data hub at the center of a full research institution, from researcher profiles and publications, to multilingual institute magazines and conference sites.

### Not your typical CMS project

At [Eurac Research](https://www.eurac.edu/en), complexity isn’t a challenge, it’s the starting point. The private research centre in Bolzano brings together around 800 researchers working across fields as diverse as earth observation, renewable energy, linguistics, biomedicine, and Alpine environments. Oh. And even mummies.

(Image content)

When Thomas Iacopino joined the web team, none of today's infrastructure existed yet. There was a SharePoint site and a communication department that was the single source for everything published. There was a plan already in motion with a Viennese company to migrate to and ship a monolithic CMS that would handle the whole thing.

For years, that complexity was mirrored by a familiar digital setup: a centralised system, a single publishing pipeline and a communications team acting as gatekeeper for everything that went online. It worked - until it didn’t.

When Thomas Iacopino joined the web team, it quickly became clear that the organisation had outgrown its infrastructure.

> “The organisation is so complex, with so many stakeholders, that it was clear you could never have a plan for all of them at any given moment. We needed a lot more flexibility than a monolithic CMS could offer.”

What followed wasn’t just a technical migration. It was a fundamental rethink of how content is created, managed and distributed across a highly decentralised organisation. Eurac Research didn’t replace one rigid system with another. Instead, it chose a modular approach built on DatoCMS, [Next.js](https://www.datocms.com/cms/nextjs-cms.md) and [Vercel](https://www.datocms.com/marketplace/hosting/vercel.md) - starting small, with a magazine prototype, and scaling from there.

(Image content)

However, what made the difference at the end wasn’t just the stack, but ownership. Instead of routing every update through a central bottleneck, teams now manage their own content within a shared framework. The central web team has shifted from *gatekeeper* to *enabler*.

> It’s less about having the perfect setup from day one, and more about creating something that can evolve with the organisation.

The transition definitely required new workflows, clear guidelines and trust, but starting with a magazine prototype made it manageable. From there, the system evolved step by step: [adding content types](https://www.datocms.com/features/schema-builder.md), [refining components](https://www.datocms.com/features/dynamic-layouts.md), and [integrating new sources](https://www.datocms.com/features/plugins.md). The modular setup now makes it easy to adapt without starting from scratch.

### The stack

The core of the Eurac setup is a single Next.js codebase deployed to Vercel, with DatoCMS handling all content. But the architecture underneath is more layered than it sounds. Their entire project is a monorepo-multisite setup that really pushes the boundaries of ["Content Hub" as a Headless CMS use case](https://www.datocms.com/use-cases/knowledge-management.md).

For their conference sites, for example, the team had been dealing with a parade of WordPress installations, each with its own templates and its own maintenance overhead. Their new solution was elegant with one shared Next.js codebase that points to separate Dato models depending on which conference it's serving. Update the codebase once, all conferences get the update. Each conference still gets its own Dato content for editorial isolation, but the frontend is unified.

(Image content)

> From a technical point of view, conferences share the same structure. So we use one Next.js codebase deployed to Vercel, and we just point it to the right DatoCMS project. You change something once and everything stays in sync.

For data syncing across their various external systems, they run three separate Next.js backend projects, each responsible for connecting to a different external system via simple HTTP APIs. GitHub Actions triggers daily syncs, keeping everything up to date without manual intervention.

The team has also built several custom plugins in-house including a Brevo newsletter integration, a Next.js preview plugin, and most recently, a text-to-speech plugin that uses OpenAI to generate an MP3 from a long article, uploads it to the [DatoCMS media gallery](https://www.datocms.com/features/images-api.md), and attaches it to the record so readers can listen to articles instead of reading them.

With any project of this scale, caching is always a nightmare. Ok to be honest, on a project of ANY scale caching is always a nightmare. So to keep things running smooooth, [cache tags were a genuine turning point](https://www.datocms.com/features/cache-tags.md) for the team. Before, they were managing a mix of time-based revalidation windows across different content types, some set to five minutes, some to a day, maintaining a document that tracked what needed to be invalidated when something changed. It was fragile, confusing for editors, and led to a constant inflow of "I published, where's my content", which is never fun.

> Caching was always a pain. With a lot of related content, a person is linked to an article, a project is linked to the same article, you have a homepage slice showing the latest news. You have to keep the whole site structure in your head to know what to invalidate when. It never really worked well.

[Cache tags](https://www.datocms.com/features/cache-tags.md) removed all of that. The team now uses them everywhere. They no longer invent naming conventions, maintain revalidation tables, or explain to editors why their change isn't live yet.

### The editorial layer

Here's where things get interesting. Eurac's web projects have about 100+ editors creating content in 3 languages, all powered by a small team of three developers. The team doesn't use granular permissions per editor, but instead, they have combined roles on a very high level to keep things smooth as far as content access and operations go.

The trust-based system at Eurac is not an accident, it's a deliberate choice. Editors are grouped by institute and given access to the models they actually need. They can read other models to create relationships, but they cannot delete content created by someone else. That single guardrail works for them without needing to create a super complex permission based structure on top of their SSO. This also means things are more "generalized" and easier to document, so onboarding takes one to two hours. After that, editors are free to publish.

What has genuinely [helped editor confidence is how consistent the DatoCMS interface has stayed](https://www.datocms.com/features/editor-experience.md) over the years. Martin called us boring and vanilla in the nicest way.

> The interface is boring, and I mean that in the best way. Editors learn it once and then it just works. With systems like X, Y, and Z, every update reshuffled the UI. With DatoCMS, they always know where the preview button is, where the publish button is. That stability makes them trust it.

Content creation itself is a mix. For the magazine, editors often draft in Word and paste into the structured content fields, which works well for long-form articles that then need image placement and translation. For more modular, block-heavy pages, direct input into the CMS is faster. The team handles most translations internally, and some editors still use Word's review and comment features for that workflow before the final version lands in Dato.

### DatoCMS as a hub

This is the part that surprised us when we first heard about the Eurac setup. DatoCMS is not just where content lives. It is the connective layer between a bunch of external systems too.

Researchers at Eurac have profiles on ORCID, the widely-used researcher identification service. They manage their publications in Converis, an internal research database, rather than having access to the DatoCMS project. Employee records live in Microsoft Dynamics. None of these people log into the CMS, and yet their data is there enriched and structured to be connected to the rest of the content on the site.

(Image content)

The three Next.js backend projects mentioned earlier each connect to one of these external systems. Every day, GitHub Actions triggers a sync. Publications and project data are pulled from Converis. Employee profiles and team data are fetched from Microsoft Dynamics. That content lands in DatoCMS, where researchers can then log in and enrich it, adding context, stories, and detail that the raw database records don't contain.

Interestingly, the content ALSO flows the other way. When a researcher has a publication that needs a DOI, a permanent identifier used for citations and academic ranking systems, the team generates the DOI and builds a landing page for it inside DatoCMS. And researcher publication data gets pushed back out to ORCID on their behalf, so researchers never need to manually update their profiles.

> DatoCMS became an event-driven hub. It's not just a place where we store content. It fetches from closed systems, enriches the data, and connects all these little islands together. At some point the ICT department came to us asking for an API to get enriched employee profiles back out of it. That told us everything.

For search, the team uses Meilisearch. [Webhooks](https://www.datocms.com/docs/general-concepts/webhooks.md) push new and updated content from DatoCMS to Meilisearch on publish.

For newsletters, the team built a custom Brevo integration via a DatoCMS plugin. Editors compose newsletters entirely within DatoCMS, picking from content that already exists on the site. The backend generates the HTML template and hands it to Brevo for delivery. Editors never touch Brevo directly. Statistics are fetched back into the interface so they never need to leave a single system.

(Image content)

The write-once-publish-everywhere model means institutes don't need separate logins to a newsletter tool, don't need enterprise plans, and don't need to copy and paste content between systems. It all lives in the same interface they already know, and gets channeled where it needs to be, because that's the "omnichannel" approach that actually makes sense when working on a content project of this size.

### Looking ahead

Thomas and co. are currently building out an AI-powered "discover our research" feature that will let visitors ask questions using natural language and get back relevant projects, publications, people, and blog posts. The structured data already in DatoCMS makes this significantly more plausible to work with than it would be otherwise.

The text-to-speech plugin we mentioned earlier is also close to being live on the frontend. Long articles will get an audio version generated automatically, uploaded to the media gallery, and surfaced to readers as a listening option.

They also have a broader vision that has guided the whole project from the start - to have no dead ends. Every page should lead somewhere. A blog post leads to the author. The author leads to their project. The project leads to a service or a related story. DatoCMS being a relational, structured content system is what makes that kind of content graph possible at scale, across three languages, managed by a team of three.

### Project

[eurac.edu](https://www.eurac.edu/en)

### Use case

[Knowledge Management](https://www.datocms.com/use-cases/knowledge-management.md)

### Industry

-   Education

### Key Features

-   [Plugins](https://www.datocms.com/features/plugins.md)
-   [Cache tags](https://www.datocms.com/features/cache-tags.md)
-   [Headless CMS multi-language](https://www.datocms.com/features/headless-cms-multi-language.md)
-   [Developer experience](https://www.datocms.com/features/developer-experience.md)

---

# HashiCorp

Source [case-studies]: https://www.datocms.com/case-studies/hashicorp.md

## Reliable, secure, and scalable workflows

Hashicorp implemented DatoCMS to manage 35k+ records and millions of monthly API calls with ease.

(Image content)

## At a glance...

### 5M

API Calls a month

### 35k

Records

### 2TB

traffic a month

### The challenge

HashiCorp needed a flexible and secure CMS to manage its ever-expanding multi-site structure.

### The result

Since the change from a full-end CMS to DatoCMS, HashiCorp benefits from quicker deployments, better integrations and a much happier editorial team.

### The Search for a new CMS

Prior to DatoCMS, HashiCorp was using a traditional CMS to build their online platform. When Jeff Escalante and his team at Carrot (now Virtue) were called to rebuild hashicorp.com from scratch, it was first tasked with taking a deep examination into their content management pipeline to identify any weaknesses or opportunities. HashiCorp was not satisfied by the former solution and needed a new CMS to manage multiple different properties, sharing assets, design and code through a wide range of products.

The team soon discovered several issues with the old solution. First, the CMS was making it hard for developers to conduct simple tasks like text editing and coding.

Second, feedback from the team revealed that the CMS was also difficult to use, took a lot of time to manage and was not intuitive for the editorial team to navigate. “We felt like the CMS wasn’t really designed for a project of that scope. As a developer, you need something more stable and flexible to follow the explosive growth of a company like HashiCorp,” says Jeff.

To Jeff and team it became clear that HashiCorp’s former CMS, while passable for simpler projects, was not designed for the multi-site structure of a fast growing open source and commercial software company. They needed a full-featured and easily scalable CMS that was also capable of handling the company’s growth as smoothly as possible.

[

Worldwide CDN

It’s the all-encompassing CDN-backed API for content you wish your company had: accessible, performant, secure, and close to every customer.

Learn more

](https://www.datocms.com/features/worldwide-cdn.md)

### A deliberate choice

Jeff and the team had experience in previous projects using DatoCMS, so they were already familiar with the system and company very well. “DatoCMS has a lot of flexibility, a good pricing range, a great user experience even with deeply linked records, and a comprehensive API,” Jeff says.

Despite his familiarity with the product, Jeff and the team did extensive research and evaluation work with HashiCorp before making a choice, and, in the end, they moved forward with DatoCMS.

> DatoCMS gives us flexibility and really good control over validation.

### The importance of a thorough API

The extensive API was one of the most important selling points that led HashiCorp to choose DatoCMS over competing products. They knew that anything that existed within the old CMS could be migrated over writing a script. “While the migration was tough and took us a while, as any major system transition will, everything about the integration with DatoCMS was quite smooth,” Jeff remembers.

DatoCMS’ Content Delivery API is also written in GraphQL, so HashiCorp could retrieve precisely the data they needed from the API while still taking advantage of all relational records. “We utilize relational records heavily for things like holding on to links between companies, products, talks, videos, blog posts, and everything else to make filtering and finding what you want easy for users, and surfacing related content easy for us” Jeff adds.

[

GraphQL content API

GraphQL provides a complete and understandable description of your API, gives clients the power to ask for exactly what they need and nothing more, and enables powerful developer tools.  

Learn more

](https://www.datocms.com/features/graphql-content-api.md)

Another immediate benefit they saw was the speed with which the DatoCMS staff responds to feedback. Jeff shares, “We are beyond excited about the close relationship we have built between Dato and HashiCorp. It’s amazing. More than once, we have asked, ‘Hey, can we have this thing?’ and the next day, we get back a response, ‘Oh yeah, here it is.’ Unbelievable.”

The project went so well that HashiCorp came in and offered Jeff a position in the company, which he “happily accepted as we had been doing exciting work on their web properties.”

[

Customer Service

We understand the needs of your clients and partners because they are just like ours. We know what worries you because we too choke up the night before that deploy.

Learn more

](https://www.datocms.com/company/about.md)

### Project

[hashicorp.com](https://hashicorp.com/)

### Use case

[Modern Websites](https://www.datocms.com/use-cases/modern-websites.md)

### Industry

-   Software
-   Technology

### Key Features

-   [Images API](https://www.datocms.com/features/images-api.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Headless CMS multi-language](https://www.datocms.com/features/headless-cms-multi-language.md)
-   [Structured content CMS](https://www.datocms.com/features/structured-content-cms.md)

---

# Polestar

Source [case-studies]: https://www.datocms.com/case-studies/polestar.md

## New global website powered by 250+ editors

Polestar's new website with DatoCMS is a powerhouse of custom plugins and workflows to support 28+ locales.

(Image content)

## At a glance...

### 35

different roles

### 250+

content creators

### 28+

locales

### The challenge

Polestar needed a multi-locale CMS to deliver its vision of engineering, design and sustainability globally.

### The result

With DatoCMS the Polestar content team succeed in creating a state of the art multi-lingual content process to showcases their technology.

### Why DatoCMS?

Creating the perfect tech stack for the perfect car that was the brief. Polestar using the expertise of Swedish agencies Stendalhs and Humblebee set their eyes on DatoCMS, with the prospect that the CMS could grow with them. As the Swedish manufacturer started to showcase and sell their high performance electrical cars across markets, they needed the possibility to scale their team across the various markets in which a Polestar is available to buy. Demand for Polestar and the team managing their digital presence sky-rocketed. DatoCMS was there to grow together with the Polestar team, ensuring a seamless scaling of locales, users, as well as a number of custom developments.

Plugins, one of DatoCMS's unsung hero features, has been central to Polestar's stunning website. The digital team created over 40 custom made plugins to manage products and assets across different markets and languages, ensuring nothing gets published by mistake.

### Project

[polestar.com](https://polestar.com/)

### Use case

[Modern Websites](https://www.datocms.com/use-cases/modern-websites.md)

### Industry

-   Automotive
-   Technology

### Key Features

-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Dynamic layouts](https://www.datocms.com/features/dynamic-layouts.md)
-   [Images API](https://www.datocms.com/features/images-api.md)
-   [Headless CMS multi-language](https://www.datocms.com/features/headless-cms-multi-language.md)

---

# Deltares

Source [case-studies]: https://www.datocms.com/case-studies/deltares.md

## Providing interactive geospatial data for FAIR use

Deltares pushes the boundaries with DatoCMS to turn complex geospatial metadata into interactive data layers.

(Image content)

## At a glance...

### 100+

Data layers surfaced

### 5+

Custom plugins

### The challenge

Deltares needed a durable and transparent way to make complex & federated geospatial data discoverable and accessible for non-technical end users.

### The result

With DatoCMS as a flexible layer, Deltares enables easy management and exploration of environmental datasets for a wide variety of end users.

When most people think of a Headless CMS, they picture marketing websites or blogs. They're probably not thinking of any combination of words including “federated marine data portals using standardized metadata vocabularies and real-time geospatial services.”

But that’s exactly where [Deltares](https://deltares.nl/), a Dutch applied research institute for water and subsurface, has pushed the boundaries of what a headless CMS can do.

(Video content)

The project above is based on the building blocks of OpenEarth. These building blocks are set up using off the shelf open source products that are set up to provide and support OGC standards for storing and exchange of spatial data. The show case can be seen live via the [Marine Data Store (informatiehuis marien)](https://viewer.openearth.nl/ihm-viewer).

At Deltares, a collaborative team of data scientists and engineers under the OpenEarth initiative work to make complex geospatial data FAIR (Findable, Accessible, Interoperable, and Reusable) and openly available. They provide expert guidance to governments, NGOs, and scientific institutions worldwide on leveraging local data applications. The team tackles massive, messy, and often obscure datasets, such as seal migration patterns in the Wadden Sea, digital elevation models of coastal environments, or mussel bed dynamics over time, requiring specialized expertise and tools to manage and analyze.

Yet, DatoCMS sits right at the heart of this system, powering (meta)data management, enabling dynamic search, and giving domain experts an approachable interface to publish, describe, and share their datasets with the world.

The magic happens when DatoCMS meets tools like Mapbox, GeoServer, Geonetwork and the Open Geospatial Consortium (OGC) API standards. But more on that in a bit.

## A Decade-Long Journey Toward Open Data

This all started over a decade ago with the OpenEarth initiative. Gerrit Hendriksen, data scientist at Deltares, describes it less like a platform and more like a philosophy.

> OpenEarth is about sharing knowledge, not just data, but also the documentation and code that come with it.

From early on, the goal was to follow FAIR data principles: make everything Findable, Accessible, Interoperable, and Reusable. In practice, that meant using global standards like those defined by the OGC—an organization that sets the baseline for how spatial data is served and consumed.

Deltares built their early tools with raw data services: no UI, no metadata frontend, just APIs and documentation. But they soon hit a wall.

> People are scared of technology like GeoServer, like PostGIS databases, things like that. We had services, but nobody could work with them. Only technicians could use the platform. We needed something visual, something for end users.

What followed was a collaboration between Deltares and our friends over at [De Voorhoede](https://www.datocms.com/partners/voorhoede.md), who worked together on building an exceptional solution to a highly technical problem.

## From Clunky XML to a Modern Metadata Layer

The original frontend required XML files to define what data layers were available. That worked. Technically. But it was error-prone, unmaintainable, and unusable by clients. And these clients were serious organizations: Dutch ministries, coastal research institutes, infrastructure agencies, you get the idea.

The team embarked on a ground-up rebuild, recognizing the need for a more robust and scalable solution. To achieve this, they adopted Mapbox for creating rich spatial interfaces, partnered with our friends at [De Voorhoede](https://www.voorhoede.nl/nl/), to tackle complex content structures and web development, and replaced their legacy CMS with a flexible, API-first system. DatoCMS was introduced as the key to unlocking this new approach, providing the team with the agility and scalability required to support their evolving needs and ultimately deliver a more effective and efficient platform for managing complex geospatial data.

> We needed something that let us organize our data in a way that our users could actually find and use it. Not just a backend database, something that worked more like a structured content system.

DatoCMS became the place where data stewards define and maintain metadata schemas. It’s where they manage the "table of contents" that tells the frontend what data layers exist, how they’re described, what thematic categories they belong to, and what external sources they're linked to.

(Video content)

And this isn’t a one-way sync. There’s a crawler system built by De Voorhoede, that connects to the OGC WFS endpoints and pulls unique values from datasets: species names, measurement types, geographic locations. Those values are then stored in DatoCMS, making them fully searchable.

> One of the most important layers in modeling and describing landscape is a digital elevation model—where are the low parts, where are the high parts? If you visualize it on a map, then everybody knows, of course. You see blue zones, red zones, and things like that.

While the tech stack might sound complex, the biggest lesson here isn’t about tools. It’s about process.

> This platform didn’t come from a product manager making a PowerPoint. It came from sitting in rooms with developers, sprinting with our partners, talking to real users, asking them how they want to work and what would actually help them.

There’s no drag-and-drop page builder here. What Deltares and De Voorhoede built is something more powerful: an environment where the backend is highly technical, but the CMS interface is extremely straightforward and usable. Scientists and civil adminstrators, many of whom aren’t developers, can describe, tag, and organize data layers without writing XML or touching code.

Deltares handles the infrastructure: the GeoServer, the metadata harvesters, the data refresh pipelines. Their partners handle the interface and frontend logic. The end clients get something that feels seamless and interactive.

## Making Data Searchable and Understandable

Another exceptional addition to this is the ability to search through the data. When your platform has hundreds of layers, each linked to different datasets, models, observations, or measurements, finding what you need can be impossible. Especially if you don’t know what that thing is called in the database.

(Video content)

Deltares solved this by combining structured metadata with real-time API lookups. They pull values from OGC services, convert scientific species names into common names (using the WoRMS API, which Gerrit still chuckles about), and enable multilingual search across hundreds of datasets.

So if someone types "mussels" or “zeehonden” (seals in Dutch), they don’t get a blank screen, they get relevant datasets, linked layers, and metadata descriptions in their preferred language.

“We don’t translate all the data”, Gerrit clarifies. “Besides the fact that you simply can’t translate raster files (because only numbers), a DatoCMS plugin only translates species names from scientific names to common names to enable discovery of layers with observations of species. These harvested names are also stored in the metadata of these specific layers.”

## Maintaining Standards in a Federated World

A huge part of this project is standardization. You’ve got external data providers, legacy systems, and ministry mandates. Gerrit admits, “Nobody likes to follow standards.” But they’re essential if you want federated systems to work.

To manage this, Deltares uses vocabularies like AQUO and BODC. The CMS enforces minimum standards so data providers can’t just dump in arbitrary values.

> You need to know what 42 means. What is 42? It could be 42 birds, 42 milligrams per liter of nitrate, or whatever. That’s why we need vocabularies and standards.

They’ve even built a metadata harvester plugin in DatoCMS. Instead of manually copying values, data managers can just link to an external metadata record, and Dato pulls it in and makes it searchable.

## Why This Works

A lot of people talk about decoupled systems. Very few actually pull them off at scale, across institutions, in a way that’s not a nightmare to maintain.

This works because Deltares didn’t just build a platform. They built a workflow - a continuous process of iteration, feedback, and dialogue between clients, developers, and scientists.

> It’s easy to set up a data service. It’s hard to maintain it over time. And it’s even harder to make it usable by people who weren’t involved in setting it up.

It's also worth pointing out that as the CMS, in no way are we "doing all the heavy lifting" here. Instead, we serve as a flexible bridge between the complex data and the users' needs. Dato's role is to provide a robust foundation for managing metadata, content structure, localized descriptions, taxonomy mapping, and other essential functions, all while being versionable, API-accessible, and intuitively usable for non-technical users, letting them focus on their core work without having to get bogged down in technical complexities.

(Video content)

The frontend is snappy and modern, thanks to De Voorhoede’s clean Vue/Mapbox implementation. The backend is stable, with Deltares' ops team handling data freshness, service availability, and long-term maintainability.

Today, the platform supports multiple environmental viewers: Marine Information, WaterInfo Extra, and others, used by public sector institutions across the Netherlands.

And behind it all is yours truly, something most people wouldn't expect to think of to handle this kind of workload for interactive geospatial data.

## Summarizing the role of the CMS

At the risk of repetition, DatoCMS is not where Deltares stores its geospatial data. The CMS enables linking datasets living in dedicated servers like GeoServer, enabling accessibility through OGC standards like WMS and WFS. What we provide is a single point of entry to administrate data structure and the metadata, enabling discovery and usability of datasets. It is where data stewards manage what each dataset represents, how it should be categorized, and how it connects to the rest of the system.

Deltares uses DatoCMS’s [structured content models](https://www.datocms.com/docs/content-modelling.md) to define metadata schemas that match their environmental and scientific standards. These models include [fields for](https://www.datocms.com/user-guides/content-modeling/intro-to-fields-in-datocms.md) layer names, descriptions, thematic tags, source references, measurement parameters, and translations. This makes it possible to manage hundreds of data layers in a way that is consistent, searchable, and understandable for non-technical users.

One of the key features is DatoCMS’s [flexibility with plugins and APIs](https://www.datocms.com/user-guides/the-basics/intro-to-the-plugin-ecosystem.md). Working with De Voorhoede, Deltares built a crawler that connects to WFS endpoints and automatically extracts unique values from datasets, such as species names or measurement types. Another plugin harvests metadata directly from external sources like GeoNetwork. This reduces manual effort and keeps metadata aligned with upstream systems.

Because DatoCMS is [fully API-first](https://www.datocms.com/academy/headless-cms/how-a-headless-cms-works.md), this metadata is immediately available to the frontend. Search and filtering work reliably across layers, powered by live metadata without the need for custom backend services. Role-based permissions allow data managers to safely handle updates themselves, without relying on developers.

DatoCMS gives Deltares a maintainable, scalable way to bridge the gap between complex geospatial data services and real-world usability. It helps ensure that environmental data is not only open but also understandable and accessible.

### Project

[viewer.openearth.nl](https://viewer.openearth.nl/)

### Use case

[Modern Websites](https://www.datocms.com/use-cases/modern-websites.md)

### Industry

-   Information Technology
-   Energy

### Key Features

-   [Structured content CMS](https://www.datocms.com/features/structured-content-cms.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Data integrity](https://www.datocms.com/features/data-integrity.md)
-   [Headless CMS multi-language](https://www.datocms.com/features/headless-cms-multi-language.md)

### Partner

[De Voorhoede](https://www.datocms.com/partners/voorhoede.md)

---

# Chilly's

Source [case-studies]: https://www.datocms.com/case-studies/chillys.md

## Scaling eCommerce to 2M+ monthly users

Rotate° worked with Chilly's to create a scalable eCommerce presence in 35+ markets.

(Image content)

## At a glance...

### 2M

Users every month

### +134%

Mobile conversion rate

### +166%

Revenue increase YoY

### The challenge

Rotate° needed a stable and easily scalable e-commerce solution for Chilly’s, a global brand serving over 35 markets across the world.

### The result

Thanks to DatoCMS and a serverless approach, Rotate° delivers a smooth and personalized customer experience to millions of users every month.

### The need to support Chilly’s explosive growth

As a long-term partner, Rotate° followed closely [Chilly’s](https://www.chillysbottles.com/uk) plan to go from an ambitious start-up to a global lifestyle brand in a couple of years.

To prevent the brand’s explosive growth from becoming an infrastructure nightmare, Rotate° had to find a solution that would free them from database management, server stability, and security.

They wanted to create a **stable, fast, and secure architecture** that could infinitely scale with the rising popularity of Chilly’s.

### A worry-free stack

Rotate° has decided to invest in a distributed technology stack to move away from the monolithic model that would have required daily care of databases, servers, and security.

With more than twenty serverless functions ready to spin up on-demand based on site traffic and DatoCMS to manage the stability and security of the content, the Rotate° team can now work on things that make the difference for an e-commerce.

“With DatoCMS, our clients do not need to pay money to us to manage infrastructure so that we can focus on the development of the product. As an agency, this is the way it should always be,” says Jim Tattersall, founder and CTO of Rotate°.

[

Worldwide CDN

It’s the all-encompassing CDN-backed API for content you wish your company had: accessible, performant, secure, and close to every customer.

Learn more

](https://www.datocms.com/features/worldwide-cdn.md)

The new architecture manages to serve content to **2,000,000 people a month**, with peaks of **125,000 concurrent users** effortlessly.

> We do not need to worry about scaling and stability with DatoCMS. Chilly’s took a prime time tv ad campaign out and they asked us “What we need to do to prepare for the traffic spikes?” and we said “nothing”.

### More than just stability

DatoCMS provided more value to Rotate° than just an easy to manage architecture.

Moving to a Content-as-a-Service model allowed Chilly’s to manage one single website serving over 35 territories, selling products in various currencies and languages with content managed effortlessly from a single hub.

[

Dynamic layouts

Define reusable custom components and build dynamic layouts for landing pages, micro websites, case studies and testimonials

Learn more

](https://www.datocms.com/features/dynamic-layouts.md)

Rotate° also used DatoCMS to build dynamic pages for new customers based on their acquisition path and purchase intent, to tailor the “post-click” experience further and increase their propensity to purchase.

> The speed of development with DatoCMS is radically faster than any other system we used in the past.

### Get to know Rotate°

Rotate° is an agency focused on design, development & marketing for eCommerce brands in the Luxury & Lifestyle sectors based in London. They help ambitious brands to thrive online with cutting-edge technologies and personalized solutions. If you want to know more about Rotate°, take a look at [studiorotate.com](https://studiorotate.com/).

### Project

[chillys.com](https://www.chillys.com/)

### Use case

[eCommerce](https://www.datocms.com/use-cases/ecommerce.md)

### Industry

-   Retail

### Key Features

-   [Data integrity](https://www.datocms.com/features/data-integrity.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Headless CMS multi-language](https://www.datocms.com/features/headless-cms-multi-language.md)
-   [Workflow CMS](https://www.datocms.com/features/workflow-cms.md)

### Partner

[Rotate°](https://www.datocms.com/partners/rotate.md)

---

# Oberlo

Source [case-studies]: https://www.datocms.com/case-studies/oberlo.md

## Migrating 38K+ assets without breaking a sweat

The Shopify and Oberlo team worked on a solution that could easily manage an asset and content heavy site.

(Image content)

## At a glance...

### 0,9

time to interactive

### 38k

assets migrated

### 2K

records migrated

### The challenge

Oberlo needed an editor-friendly headless CMS that could handle media flawlessly for a content-heavy static website.

### The result

DatoCMS helped Oberlo switch to static painlessly, offering blinding speed and an intuitive editing process.

### A CMS for static websites

With six languages and multiple domains, the old WordPress stack was not reliable enough for Oberlo’s plans. As a content-heavy website, the team decided to go static for **better performance and accessibility**.

Having many in-house content editors and freelancers and plenty of legacy content to convert, they needed a headless CMS.

[

Wordpress migration

If you have some projects that are currently using Wordpress, you can now import them to DatoCMS in a pretty simple way!

Learn more

](https://www.datocms.com/blog/wordpress-importer.md)

### Why DatoCMS

The teams at Shopify had already tried a couple of headless CMS before, with **mixed results**.

“We found other headless CMSs kind of a little bit daunting just to get started, and we didn’t want to spend too much time learning all the intricacies of systems that felt bloated and big,” says Frank Reding, senior developer at Oberlo.

[

Dynamic layouts

Define reusable custom components and build dynamic layouts for landing pages, micro websites, case studies and testimonials

Learn more

](https://www.datocms.com/features/dynamic-layouts.md)

While exploring their options and searching for a CMS that could work well with their static site generator of choice, 11ty, they bumped into DatoCMS, and the feature-set was just right for them.

“Immediately, DatoCMS ticked all the boxes without being overwhelming,” Frank adds. The pricing structure of DatoCMS was also crucial for them: “we could experiment with it without having to get way up the chain to get approval,” remarks Frank.

> Immediately, DatoCMS ticked all the boxes without being overwhelming.

### A CMS for content kings

DatoCMS’s clean user interface and ease of use are crucial for Oberlo, which lives and dies on its content quality and output. Content editors can now write and publish pages without developers’ help, with integrated services like **Mux for streaming** and **imgix for image manipulation** that do the heavy lifting for them.

[

Images API

Serve lightning fast images for any digital product with a suite of tools built to save both development time and visitor bandwidth.

Learn more

](https://www.datocms.com/features/images-api.md)

Lazy loading, progressive images, and static pages changed performance dramatically: the homepage went **from 15 seconds to interactive to less than one second**.

“Our performance has gone through the roof since we went to static. Even with all the caching layers and the work done on WordPress, it was never going to reach this level,” says Frank.

> Our performance has gone through the roof since we went to static.

[

Worldwide CDN

It’s the all-encompassing CDN-backed API for content you wish your company had: accessible, performant, secure, and close to every customer.

Learn more

](https://www.datocms.com/features/worldwide-cdn.md)

### Project

[oberlo.com](https://oberlo.com/)

### Use case

[eCommerce](https://www.datocms.com/use-cases/ecommerce.md)

### Industry

-   Manufacturing
-   Retail
-   Software

### Key Features

-   [Video API encoding and streaming](https://www.datocms.com/features/video-api.md)
-   [Images API](https://www.datocms.com/features/images-api.md)
-   [Data integrity](https://www.datocms.com/features/data-integrity.md)

---

# Beletrina

Source [case-studies]: https://www.datocms.com/case-studies/beletrina.md

## Culture on demand across 10K+ files

Trampolin built Beletrina a technically ambitious media platform for films, audiobooks, and e-books.

(Image content)

## At a glance...

### 4.5K+

e-books available across multiple locales

### 4K+

Audiobooks streamed through a custom backend

### 500+

HD Films curated into thematic catalogs

### The challenge

Beletrina Digital needed a flexible way to manage rich multimedia content while syncing content across multiple portals.

### The result

Trampolin used DatoCMS to power a modular system with custom plugins to sync media and HD content across multiple B2B & B2C portals.

### Building a Boutique Streaming Platform with DatoCMS

[Beletrina Digital](https://beletrinadigital.si/) is not your average streaming platform, nor is it another clone. It blends highly curated collections of e-books, audiobooks, films, podcasts, and longform editorial content into one focused space. Instead of throwing everything into a catalog and letting users scroll endlessly, it guides them. Every recommendation, every playlist, every article is intentionally and meticulously chosen.

The result feels like a digital cultural magazine, but with full multimedia capabilities. And at the core of the editorial workflow sits DatoCMS.

(Video content)

[Trampolin](https://www.datocms.com/partners/trampolin.md), the agency behind the build, created a decoupled platform that lets editors and developers work independently.

Editors use [modular content blocks](https://www.datocms.com/user-guides/content-management/building-pages-and-deep-dive-into-modular-content.md) in DatoCMS to manage and publish new material, while media delivery, subscriptions, and streaming are handled by a separate backend. The frontend is built with [Nuxt](https://www.datocms.com/docs/nuxt.md), user auth is powered by Supabase, and DatoCMS acts as the content layer tying everything together.

What makes this project effective is not flashy design or overwhelming content volume. It’s the simplicity of the user experience paired with the flexibility of the underlying tech.

(Video content)

DatoCMS provides the structure, the speed, and the editorial freedom needed to publish fast without cutting corners. It fits into a modern stack without forcing compromises or adding weight.

The result is a platform that feels more like a cultural journal than a tech product. And that’s exactly the point Trampolin set out to make with this project.

### Built for Editors

Beletrina Digital publishes around five complex articles every week. These aren’t quick posts. Each one includes long sections of text, embedded videos, images, and metadata.

(Video content)

The team needed a CMS that was flexible enough to handle mixed formats, but [intuitive enough for non-developers to use](https://www.datocms.com/features/editor-experience.md) daily. According to Rok Klemenčič, art director and design lead on the project, DatoCMS was the right choice.

> Working with DatoCMS with its clean user interface is simple and user friendly. Having a set of predefined modular blocks is a piece of cake for the editors to build new pages.

Editors build entire pages using modular blocks, each designed to handle a specific type of content. [Layouts can be rearranged without touching code](https://www.datocms.com/features/dynamic-layouts.md), thanks to [visual drag-and-drop controls](https://www.datocms.com/features/dynamic-layouts.md).

> Reorganizing the menu structure with drag and drop is easy to reposition sections on a page, all without any help from developers.

Navigation and menu structures can be adjusted in seconds. There’s no reliance on developers for basic updates, which means content moves faster.

### Streaming as Structured Content

While DatoCMS powers the editorial layer, the media itself lives in a dedicated backend system. That backend handles subscriptions, paywalls, access control, and HD media streaming.

(Video content)

To bridge the two systems, Trampolin built a [custom DatoCMS plugin](https://www.datocms.com/features/plugins.md). Editors can browse available media directly from the backend, then select which items to feature in their articles or collections. Each selected media item pulls in all the associated metadata and preview assets automatically, avoiding double entries or errors.

This makes the editorial process more efficient and far less error-prone. Editors work in one place, and the connection between content and media stays clean.

### Scaling to B2B

Beyond its main site, Beletrina Digital also powers custom portals for B2B clients, which are often companies or public institutions that want to give employees or patients access to curated cultural content.

(Video content)

One hospital in Slovenia, for example, runs two different versions of the platform. One targets general staff. The other is specifically purpose-built for the pediatrics department. Both share the same backend infrastructure and sync with the core CMS instance, but each is highly customized locally on the UI.

When a new article is published on the main platform, it gets pushed to all sub-sites automatically. Editors at each portal can then tailor the selection or override content as needed. This setup would be painful to manage manually in most CMSs. With DatoCMS and structured content models, however, it just works.

### Project

[beletrinadigital.si](https://beletrinadigital.si/)

### Use case

[Digital Publishing](https://www.datocms.com/use-cases/digital-publishing.md)

### Industry

-   Entertainment
-   Education

### Key Features

-   [Images API](https://www.datocms.com/features/images-api.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Video API encoding and streaming](https://www.datocms.com/features/video-api.md)
-   [Editor experience](https://www.datocms.com/features/editor-experience.md)

### Partner

[Trampolin](https://www.datocms.com/partners/trampolin.md)

---

# Arduino

Source [case-studies]: https://www.datocms.com/case-studies/arduino.md

## Shipping updates 50% faster with 92,5% less code

Learn how Arduino cut its time-to-market in half with DatoCMS and got 8x faster loading times.

(Image content)

## At a glance...

### 50%

Faster time-to-market

### \-92,5%

Lines of code

### x8

Faster loading times

### The challenge

The world’s leading open-source hardware company needed a cloud-based decoupled CMS to manage its fast-growing educational products.

### The result

Since the change from a classic CMS to DatoCMS, Arduino can deliver new products faster, with better performances for teams and users alike.

### The need for an elastic content platform

Arduino was looking for a solution to enhance the user experience of the Educational branch of their business because their old traditional web content management platform with coupled architectures was failing to meet the needs of their team.

The old monolithic CMS did not allow to go on the market with new educational products in a short time, because the editorial and development teams were practically forced to start from scratch every time they wanted to add a new property. Not only the work done on previous projects could not be properly, but the constant fiddling on the frontend caused codebase bloat.

The performance also started to be a problem: while project homepages could load rather quickly, internal pages could take as long as eight seconds.

Arduino needed scalability, first-of-the-class performances, and a solution that could easily spin up multiple sites in a matter of days.

With all that in mind, they found DatoCMS to be the best-fit solution to take their content infrastructure to the next level.

### A solution-driven agency enters the fray

For such a radical change, Arduino decided to rely on the experience of an external team. The choice fell on [Cantiere Creativo](https://www.datocms.com/partners/cantiere-creativo/showcase/arduino.md), the tech agency where DatoCMS took its first steps as a product.

"Generally, we prefer to talk about solutions before technology stacks, but in this case, DatoCMS was what Arduino exactly needed," said Matteo Manzo, Cantiere Creativo technical project manager.

The API-first nature of DatoCMS has allowed Cantiere Creativo to choose the right language for Arduino's educational projects.

"DatoCMS does not force you to speak its language, but gives you the peace of mind of being able to use the right solution for each project," says Matteo. "For Arduino, we chose React, because the language is very modular, fast to implement, and very smart: it loads only the interface blocks you weren't already using in the previous page."

[

Dynamic layouts

Define reusable custom components and build dynamic layouts for landing pages, micro websites, case studies and testimonials

Learn more

](https://www.datocms.com/features/dynamic-layouts.md)

To allow Arduino to create new sites as quickly as possible, Cantiere Creativo has made extensive use of DatoCMS Modular Blocks. "We were able to create modular content in record time so that the Arduino team can use them as building blocks for every kind of page structure," adds Matteo.

> DatoCMS does not force you to speak its language, instead gives you the peace of mind of being able to use the right solution for each project.

### Faster, more optimized user experiences

The React and DatoCMS combo allowed Cantiere Creativo to work on optimizing the front end code.

"The results were amazing: **we were able to move from 26,107 lines of code of the old version to just 1,267**" remembers Matteo.

[

Simplify your code

When it comes to authoring content, pair React with a CMS that’s been built for single-page applications.

Learn more

](https://www.datocms.com/cms/react.md)

The internal pages average loading times have also seen a drastic change of pace: from 8.3 seconds to timings always under a second.

The new modular approach to coding and content management is going to help Arduino launch educational products faster, feeding content from a central hub, and with better performances.

"We're still working with Arduino on a couple of customized plugins to automate translations and markdown validations, but even now I would say they can launch a new product at least in half the times it took just a year ago," says Matteo

### Project

[arduino.cc](https://www.arduino.cc/education)

### Use case

[Knowledge Management](https://www.datocms.com/use-cases/knowledge-management.md)

### Industry

-   Technology
-   Software
-   Hardware

### Key Features

-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Headless CMS multi-language](https://www.datocms.com/features/headless-cms-multi-language.md)
-   [Workflow CMS](https://www.datocms.com/features/workflow-cms.md)

### Partner

[Cantiere Creativo](https://www.datocms.com/partners/cantiere-creativo.md)

---

# Wonderland.

Source [case-studies]: https://www.datocms.com/case-studies/wonderland.md

## Setting up dozens of projects in minutes

Wonderland uses DatoCMS to manage all their asset-heavy projects without breaking the bank.

(Image content)

## At a glance...

### 300

hours of maintenance saved

### 3m

setup time for new projects

### 6x

faster loading times

### The challenge

Wonderland needed an enterprise-level headless CMS to manage all their asset-heavy projects without breaking the bank.

### The result

Wonderland published dozens of visually stunning websites with DatoCMS, giving clients an intuitive and affordable platform for all their needs.

### Getting away from Wordpress

[Wonderland](https://wonderlandams.com/) worked for years with Wordpress but felt the need to try a headless approach to content management. “It was a headache maintaining plugins, security, and upgrading the front end accordingly for all our project,” says Maarten Vleugels, creative developer at Wonderland. Wonderland struggled at first to find a headless CMS that was powerful, affordable, and yet flexible for projects of any size. “We have used another enterprise-level headless solution in the past. It worked fine, but the pricing was prohibitive, the features were limited and had a bad GraphQL support” adds Maarten.

[

Wordpress migration

If you have some projects that are currently using Wordpress, you can now import them to DatoCMS in a pretty simple way!

Learn more

](https://www.datocms.com/blog/wordpress-importer.md)

### “DatoCMS is so easy.”

Wonderland found in DatoCMS the right solution for their needs. Thanks to its SaaS nature, It helped them zeroing out the maintenance routine, taking care of security and platform updates for them. GraphQL has enabled the Wonderland team to work on development efficiently while offering customers an easy-to-use platform for non-technical teams as well. “Clients love DatoCMS. With a proper setup, they prefer DatoCMS over anything, especially with a Wordpress background” adds Maarten.

“I’ve tried all of the headless CMSs. Seriously, all of them. DatoCMS was the one we really liked because it’s easy to use for everybody: clients are pleased with it, and we developers can use its powerful APIs.”

[

Value for money

Zero maintenance, zero operations: save tens of thousands of dollars annually by using DatoCMS headless technology and content infrastructure.

Learn more

](https://www.datocms.com/pricing.md)

The competitive price for agencies helped Wonderland use DatoCMS for a plethora of projects of all sizes without having to change platform or lowering their efficiency.

> Clients love DatoCMS. With a proper setup, they prefer DatoCMS over anything, especially with a Wordpress background.

(Video content)

### An Open Platform

“DatoCMS offers quick updates and new features monthly, and any feature request is handled properly” says Maarten “but if you can’t wait for it, or you want specific content fields, the API for writing plugins is easy”. Wonderland used the plugin capabilities of DatoCMS to scale and design their CMS to their own needs, for example writing a button that copies all the content translations from locale to locale.

[

Customer Service

We understand the needs of your clients and partners because they are just like ours. We know what worries you because we too choke up the night before that deploy.

Learn more

](https://www.datocms.com/company/about.md)

### Project

[wonderland.studio](https://wonderland.studio/)

### Use case

[Modern Websites](https://www.datocms.com/use-cases/modern-websites.md)

### Industry

-   Entertainment
-   Technology

### Key Features

-   [Structured content CMS](https://www.datocms.com/features/structured-content-cms.md)
-   [Fastest headless CMS](https://www.datocms.com/features/worldwide-cdn.md)
-   [Images API](https://www.datocms.com/features/images-api.md)
-   [Video API encoding and streaming](https://www.datocms.com/features/video-api.md)

### Partner

[WONDERLAND](https://www.datocms.com/partners/wonderland.md)

---

# Matter Supply

Source [case-studies]: https://www.datocms.com/case-studies/matter-supply.md

## Delivering an Emmy award-winning campaign in 4 weeks

Matter Supply used DatoCMS to deliver a compelling user-centered experience in a pinch.

(Image content)

## At a glance...

### 200k

Daily users

### 190k

User submissions

### 0,9s

First Contentful Paint

### The challenge

Matter Supply Co. needed a fast, easily accessible CMS to deliver a compelling user-centered experience for a campaign in less than 4 weeks.

### The result

Using DatoCMS and a modern stack, Matter Supply almost doubled the initial goals of the client and served more than a million users in two months.

### A race against time

Despite the ten-year experience of its founders and a couple of impressive works under its belt, Matter Supply Co. was still a relatively young agency when it was contacted by the the biggest sportswear brand in the world to take care of the digital part of a critical marketing campaign.

The agency immediately took the opportunity to work on such an exciting project, but quickly found itself facing some real challenges. The largest, according to co-founder Marc Ammann, was the limited time available: "The client asked us to launch the campaign within a month."

The idea was a user-centered experience, where the user could submit the "dreams" they would like to follow, and the platform would show a real-time live stream of everybody's dreams visualized as dots on the website. They needed to integrate a sign-in feature, work from the ground up on both front-end and back-end, and be ready to handle huge traffic spikes.

"All of this with four weeks of work in the budget from the first call with the client and only two developers readily available for the project," recalls Marc.

### A faster stack

The Matter Supply team had evaluated a traditional stack for the project, but they were not sure they could achieve their goals in just four weeks. For this reason, they chose a JAMstack architecture, based on client-side JavaScript, reusable APIs, and prebuilt Markup. This type of stack offers a level of speed, security, and scalability with few equals, but they had to find the right CMS to give the green light.

> We tried DatoCMS, and the team loved it; it felt good, it felt very nice, and our client has been super happy with it.

### An agency-driven approach

"We needed something focused on the user experience, with user-friendliness in mind," says Marc Ammann.

They also searched for a CMS easily accessible for the clients when they update their content, and this is one of the reasons why Matter Supply chose DatoCMS. The story of DatoCMS, born as an internal product of an agency, not unlike Matter Supply, was also a deciding factor.

"This is what I wanted," adds Marc, "A CMS built by an agency because they know what my clients need."

[

Customer Service

We understand the needs of your clients and partners because they are just like ours. We know what worries you because we too choke up the night before that deploy.

Learn more

](https://www.datocms.com/company/about.md)

### Like Squarespace

One of the essential features for Matter Supply was the Modular Content. They can put content together quickly and iterate on it in a matter of minutes, helping the client's editorial team to feel at home.

"It's what all clients want: to have something that gets very close to Squarespace," says Marc.

[

Dynamic layouts

Define reusable custom components and build dynamic layouts for landing pages, micro websites, case studies and testimonials

Learn more

](https://www.datocms.com/features/dynamic-layouts.md)

The API-first nature of DatoCMS also helped the team to work simultaneously on the back-end and the front-end, to iterate on them, and collaborate with the brand's internal teams.

> I think Modular Content is probably one of my favorite features. Being able to put together a piece of content that the client can see on one page and ‘hey, this gets very close to Squarespace.‘

### A sound success

Matter Supply managed to deliver the project in less than four weeks, integrating all the services flawlessly. The Campaign was so successful that it won an Emmy Award for "Outstanding Commercial," and the digital part reached millions of people worldwide without a hitch. Not only Matter Supply reached the goals, but they far exceeded them thanks to their ingenuity and a modern, flexible stack.

[

Worldwide CDN

It’s the all-encompassing CDN-backed API for content you wish your company had: accessible, performant, secure, and close to every customer.

Learn more

](https://www.datocms.com/features/worldwide-cdn.md)

### Project

[mattersupply.com](https://www.mattersupply.com/)

### Industry

-   Entertainment

### Key Features

-   [Fastest headless CMS](https://www.datocms.com/features/worldwide-cdn.md)
-   [Structured content CMS](https://www.datocms.com/features/structured-content-cms.md)
-   [Images API](https://www.datocms.com/features/images-api.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)

---

# Dovetail

Source [case-studies]: https://www.datocms.com/case-studies/dovetail.md

## Focusing on UX and strong integrations

Dovetail saves 100+ dev hours for themselves and their clients every month with DatoCMS.

(Image content)

## At a glance...

### 2x

faster image load times

### 100+

developer hours saved every month

### 1.5s

page load time

### The challenge

Dovetail needed a feature-rich and easy-to-use CMS for their client projects and their own website.

### The result

After moving to DatoCMS, Dovetail now has a website that can be updated without developers and their client projects are using a reliable and modern content management system.

### The need for a CMS

[Dovetail](https://dovetailstudios.com/) had their favourite frameworks for building apps and web platforms that can handle millions of users, but they didn’t have a CMS that they really liked. Their own website was in need of a refresh and they wanted a solution where edits wouldn’t have to go through a developer. They decided to rebuild their own website to experiment with new CMSs and find alternatives that would be scalable, easy-to-use, and customisable enough to use on complex much larger projects.

### Starting with Gatsby

The first thing Dovetail did when looking for a headless CMS was to find something that supports GatsbyJS. "We like Gatsby, it’s stable and modern. We just needed to find a CMS that integrates with it easily.” says Nick Clark, senior software engineer at Dovetail.

There were a number of options on the market but most seemed too inflexible and many didn’t have an attractive pricing plan. “I found an article comparing DatoCMS with Contentful. DatoCMS sounded more developer friendly as they really listened to feedback” Nick remarks.

Getting DatoCMS up and running with Gatsby was really easy thanks to the source plugin. The Dovetail team immediately had access to the GraphQL API they know and love and were able to use it to power all the content on their website.

[

GraphQL content API

GraphQL provides a complete and understandable description of your API, gives clients the power to ask for exactly what they need and nothing more, and enables powerful developer tools.  

Learn more

](https://www.datocms.com/features/graphql-content-api.md)

### A superior CMS experience

In addition to how easy it is and use and get started, the Dovetail team was especially impressed by how feature-rich DatoCMS is. The ability to run GraphQL queries to grab images in an intelligent way can speed up a website by not serving the wrong sized images. Image source sets are automatically generated and DatoCMS renders different sized images based on device meaning that no unnecessary bandwidth is ever wasted.

[

SEO

If you are building a website, you need to think about SEO and provide custom special content for search engines and social networks. DatoCMS has you covered!

Learn more

](https://www.datocms.com/docs/content-modelling/seo-fields.md)

The SEO features that DatoCMS offers allows companies to fall back on intelligently defined defaults even if certain meta tags aren’t set. For example, Open Graph meta tags are automatically generated with the first image on a page. It’s smart defaults like this that make DatoCMS so powerful to use.

> We first rebuilt our own website with DatoCMS to try it out and we love it so much we’re now rolling it out to our biggest client projects.

### Get to know Dovetail

[Dovetail](https://dovetailstudios.com/) is a full-service product development agency that specialises in building digital products that scale. Owned and operated by a team of entrepreneurs that have built and sold technology businesses before, they now build and invest in fast-growing companies. As an example, Dovetail helps design, build and maintain Afterpay’s web and mobile products during which time they’ve grown to a +$15B international payments leader.

### Project

[palomagroup.com](https://www.palomagroup.com/digital/home)

### Use case

[Modern Websites](https://www.datocms.com/use-cases/modern-websites.md)

### Industry

-   Software
-   Technology

### Key Features

-   [Images API](https://www.datocms.com/features/images-api.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)
-   [Structured content CMS](https://www.datocms.com/features/structured-content-cms.md)

---

# Onninen

Source [case-studies]: https://www.datocms.com/case-studies/onninen.md

## Effortless marketing & management of 200,000+ products

Unity Group and Oninen worked on content being published across all touch-points in real time

(Image content)

## At a glance...

### The challenge

Unity Group was entrusted with the task of replacing the technology on which the Onninen online store for retail customers was based, as well as its connection with the B2B platform. The company has both B2B and B2C sales channels and new system should lay the foundations for a singular platform.

### The result

The new platform is based on DatoCMS, which enables the creation and distribution of content across all digital channels from one central panel via API, allowing for content to be published and edited across all touch-points in real time.

Onninen is a Finnish company and one of the world leaders among the suppliers of technical materials. It has been operating continuously since 1913. The organization offers integrated material services for installers, retailers, industry and public institutions in the fields of electrical engineering, plumbing, heating, ventilation and air conditioning. Its range includes **more than 200,000 products**.

Unity Group was entrusted with the task of replacing the technology on which the Onninen online store for retail customers was based, as well as its connection with the B2B platform.

The project started with technological consulting, defining the concept of architecture based on Headless CMS and a list of recommended technologies.

> Managing content from one place allows quick updates for all digital products, as well as adding new devices and sales channels in the future.

The frontend application was built in React.js and Next.js technologies. It integrates content from CMS with data and functionalities implemented by Onninen's transactional API (e-commerce)

The new platform is based on a modern content management system - DatoCMS - which enables the creation and distribution of content across all digital channels from one central panel via API.

The solution allows for content to be published and edited across all touch-points in real time.

**Resources are stored in an AI-based library**, which simplifies cataloguing, storing and managing them. They can be reused at any time by the marketing team.

The platform provides ready-made components, which allows employees to **efficiently create a template for each page**.

> The headless architecture will make the backend consistent and allow Onninen to launch new sales channels in the future without limits.

Product pages, dynamic landing pages, content marketing activities and search engine positioning **can all be managed by tools within the platform**.

### Get to know Unity Group

Unity Group has been realizing digital commerce transformation solutions <since.1997\> with over 500+ projects completed to date. They specialize in a range of a technologies, including top e-commerce platforms and headless CMS, to create solutions with impact. If you want to know more about them, take a look at [www.unitygroup.com](https://www.unitygroup.com/).

### Project

[onninen.com](https://www.onninen.com/)

### Use case

[eCommerce](https://www.datocms.com/use-cases/ecommerce.md)

### Industry

-   Manufacturing
-   Hardware
-   Energy

### Key Features

-   [Structured content CMS](https://www.datocms.com/features/structured-content-cms.md)
-   [Real-time API](https://www.datocms.com/features/real-time-api.md)
-   [Data integrity](https://www.datocms.com/features/data-integrity.md)
-   [Headless CMS multi-language](https://www.datocms.com/features/headless-cms-multi-language.md)
-   [Headless CMS GraphQL](https://www.datocms.com/features/headless-cms-graphql.md)

---

# On the aesthetics of a lighter internet

Source [casual-chats]: https://www.datocms.com/casual-chats/on-the-aesthetics-of-a-lighter-internet.md

## Customer Stories


(Image content)

In conversation with Laís Kantor (Founder & Creative Director @ goodbase)

### TLDR

-   [goodbase](https://www.datocms.com/partners/goodbase.md) is an Italy-based studio founded by Laís Kantor, working on brand, UI/UX, and websites for startups
-   [ClimateAdaptation.life](https://climateadaptation.life/) is a platform reporting on climate adaptation projects. goodbase are co-founders, not simply suppliers
    
-   The core principle when building the platform was that the infrastructure itself couldn't be hypocritical with the content
-   They started with constraints (typography weight, color energy, image loading) rather than designing first and cutting later
    
-   Users can choose color palettes and see the energy impact of each. A grid API shows real-time energy source data
-   DatoCMS handles media management, modular page building, and content relationships
    

[goodbase](https://goodbase.studio/) is based in Italy. Their team works across brand identity, marketing campaigns, UI/UX, and websites. Mostly startups and companies up to around 50-80 people. Laís, one of their founders, is the brain behind their design philosophy, who spends her time between Italy and Brazil (where she was during this call and made us pretty jealous of the sunshine).

When it comes to choosing a CMS for their projects, Dato has become the default. Their head of technology knows it well, Laís has used it before, and it just works. No drama. No fuss.

---

### Let's talk about the project

To begin with, this wasn't your typical client engagement. goodbase came in early, talked with the founder & CEO, Sergio Matalucci, and ended up becoming co-founders of the project, so they definitely have a voice in where it's heading, which is very different energy to just serving a request.

(Video content)

The platform reports on climate adaptation projects, features white papers, case studies, and all sorts of different types of content related to how we prepare for what's coming (not that I'm trying to drop any dark foreshadowing or anything).

And during all of that evaluation on the content of the project is where the question came up: if the *content* is about climate adaptation, shouldn't the *design and infrastructure* reflect that too?

### Not just "Pretty Pixels"

As with most best-in-class projects, goodbase started with the content structure. How editors will add things. How content can be reused. How components are built out using blocks. How locales are handled. The usual.

(Video content)

Then they moved to the visual layer, where every decision became a sustainability decision. Typography has weight. Colors consume energy. API calls add up. Even filtering UI matters. Things you wouldn't necessarily consider until you had an after thought on sustainability is what was front and center throughout this process.

> If the site is entirely focused on climate adaptation, shouldn't our design and technology choices reflect that same philosophy?

For example. They had a filter design where clicking on/off felt smoother as an experience. But it called the API too many times. Not great for sustainability. So they changed the design.

> As a designer, I wanted things a certain way, which is what happened with our content filtering. Visually, I preferred just clicking options on and off to filter instantly rather than selecting everything and clicking apply. But what happens? It calls the API too many times, and excessive API calls aren't great for sustainability. So I said, 'Okay, let's change this design.

That's the ongoing demand. Every choice gets questioned, and the most sustainable approach wins.

Typography was also chosen carefully. They didn't want extra weight.

Now they're adding Arabic, and the display font doesn't support it.

Do they add another typeface?

Does that make it worse?

Maybe not. If the developer can conditionally load only what's needed based on language detection.

These aren't abstract conversations. They happen in real time, mid-project, and they come with considerable trade-offs.

The same philosophy applied to images. They're not the heroes of this site. They are shown in low-res until you choose to load them. The content is the point, not the photography. But it's also worth mentioning that these lightweight images feature a sleek, monochrome noise texture, making the energy-saving choice feel like an intentional design aesthetic rather than a compromise.

### Retaining Rock-Solid Foundations

If we're being brutally honest, most sustainable websites look... ugly. Sacrifice visuals for performance. Make everything plain. Ship something that works but doesn't feel like anyone cared. Done.

ClimateAdaptation.life doesn't look like that, and the trick was inverting the process. Instead of designing first and then cutting for sustainability, they started with all those constraints, and then designed within those limits.

> We completely inverted the process. Usually, you design something first and then start stripping things away to make it more sustainable. Instead, we asked right at the beginning: 'What are our environmental constraints?' and built everything around those limits from day one

The funny thing is that even though the typography *is* climate-conscious, it *does look pretty* and well thought out. To the point that when they did user testing, people who normally read climate content said the display font felt "too designed." They're used to ugly things. But Laís pushed back. They're a new company. They need branding. They need people to remember they exist.

### OK, but Why Not Just Force Dark Mode?

The easy answer would be to make everything black and call it a day. Less energy. Done.

But. That's not inclusive. If someone's system preference is light mode, forcing dark mode ignores their choice.

(Video content)

So goodbase built something completely different. A grid API that knows your energy source, and added color palettes with visible impact labels rather than just a simple light/dark toggle. This way, the user picks the color palette they want, AND they see what that choice means in terms of energy consumption.

Honestly, that's pretty f\*ng cool!

(Video content)

> Should I force black on you just because it uses less energy? The easy answer is to make everything black and call it a day. But from an accessibility perspective, I shouldn't, because your preference might be light mode.

This is an interesting approach as well, because in the end, it's about awareness, not *just* optimization.

(Video content)

When you're choosing a color and you see the energy impact, you think about it. Maybe for the first time. Maybe it sticks.

### Making Dato Shine

OK, we LOVE the approach and the implementation, but we still had to be a bit cheeky and figure out how Dato fit into the project because we love a lil pat on the back, too.

Media management was a big reason for choosing Dato here. If you can't substitute an asset in a single place everywhere, you end up with orphaned content. Stuff you uploaded once that's now just sitting there, unused. Dato's approach to media made it easier to keep things clean. And given that Dato ships assets with 50 shades of optimization options, the actual end delivery to the user also had an impact on the sustainability of the project, since assets could be delivered lighter, and re-optimized on the fly for the user's connection speed and device.

(Video content)

On the technical side, modularity mattered too. Pages are built from reusable components. Articles are different from landing pages, but they share pieces. If they need to spin up something for an event, they can pull from what already exists.

> Modular design was key for us. We constructed everything using reusable components, which makes page-building incredibly convenient since you can easily repurpose elements as the site grows.

And there's of course the relationship layer. A lot of content, all connected. Behind the scenes, things are wired up to stay smart as the library grows.

OH, and.

They also built an open-source [geographic areas plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-geographic-area.md) early on and made it available to the [community](https://www.datocms.com/marketplace/plugins.md). This is a super nifty plugin for editors who want to select geographic areas. Ironically, they stopped using it internally because the editors wanted something that'd let them make more complex selections, but even though they didn't need the plugin anymore, they decided to keep it for the community.

That's the part Laís kept coming back to as well. The ease of talking to people, suggesting a plugin idea, and maybe collaborating on something. It feels like a real community. Not every tool does.

> "What I really like is that it's super easy for us to, you know, talk with each other, decide to put something out there and somehow work together. So I think there is this part which makes you feel a little bit like home.

🫶🏽

They're currently working on AI and audio accessibility features to implement into the project, and while they're still figuring out how to approach it given their focus on sustainability, they'll share it once it "clicks".

---

# On trading templates for topography

Source [casual-chats]: https://www.datocms.com/casual-chats/on-trading-templates-for-topography.md

## Customer Stories


(Image content)

In conversation with Alexis Malin (Co-founder & Design Director Atelier San Rita)

### TLDR

-   Their new website is a GORGEOUS interactive 3D topographic map inspired by their fly fishing days in California, and built using yours truly 💁‍♀️
-   The rest of their stack includes Blender for 3D, React Three Fiber, custom GLSL shaders, and Next.js with RSC
    
-   Despite being WebGL-heavy, the site has strong SEO and snappy performance
-   DatoCMS handles all content, with webhooks keeping the cache in sync automatically
    

### The Origins

[San Rita](https://www.datocms.com/partners/san-rita.md) started where most agencies don't. In the middle of the Californian desert. Fly fishing.

Alexis Malin and his co-founder Julien were out casting lines when they decided it was time to build something of their own. Both had serious agency pedigrees. They knew the playbook. They just didn't want to follow it.

When they started working on their brand, they wanted experimental. Not "yeah cool so we added a parallax effect" experimental. Actually different. The kind of website that doesn't feel like a website but lets their personality shine. That idea from day one was *exploration*. Old topographic maps. Trails. Camping spots. The way you read a map when you're trying to find a fishing spot. They looked at that and thought: there's something here.

> It's really inspired by nature at first, but then it's more inspired by those old maps, old topographic maps with the different way to read the map. You know, if you want to just like find the fishing spot or find the trails, the camping spot. And we're like, I think there is something to do there with that.

### Not just pretty pixels

[San Rita's site](https://sanrita.ca/) doesn't look like an agency website, which was/is the point. No black. No white. Icy greens, yellows, earthy tones.

They built something that reflects the camping trails of California, something that's personal to their origins, yet delightful for web visitors to discover.

(Video content)

They crafted all the iconography themselves in Figma. When they stepped back and looked at it, they realized it looked like an exploration map. That's when they knew they had something.

The homepage isn't a homepage. It's a map. The menu isn't a menu. It's a legend. Pages aren't pages. They're trails. The contact page is a postcard. Every element ties back to this idea of exploration and discovery.

And it translates to how they work with clients too - they explore new territory together. They push clients to try new trails instead of taking the same path everyone else is on.

### Mapping it all together

They started with a 2D map. Mountains here, a lake there, some forest over there. They named landmarks after collaborators. and then they thought: why not make this 3D?

> At first we wanted to create our own world. So it's not a real map. It's inspired from the place we fly fished in California. We tried to do a 2D map first, we want a mountain here, a lake here, the forest here, and put the names of our different collaborators on the map, like the Morrell forest. And then we were like, okay, let's try to use this way of doing the 2D map in a 3D style.

Sebastien Lempens, their creative dev, took that 2D concept into Blender, and built out the terrain, added textures, and made it feel like an actual topographic map you could touch. Then he translated the whole thing to code using React Three Fiber built on Three.js for the 3D integration. For the interactive navigation on the map, Sebastien wrote custom GLSL shaders. That's what gives the whole thing its organic, fluid feel when you're moving around.

(Video content)

For the overall structure, [Next.js was the natural choice](https://www.datocms.com/docs/next-js.md). They leveraged [React Server Components](https://www.datocms.com/academy/frontend-frameworks/react.md) to move all the heavy logic and data fetching to the server. The result is an extremely lightweight application for the user despite all the 3D happening.

### Retaining rock-solid foundations

Here's the thing about WebGL websites - they usually tank on [SEO](https://www.datocms.com/user-guides/content-management/understanding-seo-in-datocms.md). All that fancy 3D comes at a cost, and the Big G usually doesn't care how cool your shader work is.

(Video content)

San Rita's site doesn't have that problem.

> For the overall structure, we used [Next.js](https://www.datocms.com/cms/nextjs-cms.md), which was the natural choice. And leveraging React server components, he was able to move the heavy logic and the data fetching to the server. So the result is an extremely lightweight application for the user. And since it's on that, it helps the strong SEO, because usually when you create a WebGL website, the SEO is kind of low.

By moving the heavy lifting to the server with RSC, they kept the client light. Strong SEO. Fast load times. No scroll jacking. No janky scroll glitchy wonky feels. The site feels smooth even though there's a full 3D map running underneath it.

### Making us shine

WebGL and content management don't usually play nice together. When everything is baked into the 3D experience, updating an image or swapping out some text becomes a whole thing.

> We had our developer, Valentin, and he told us about DatoCMS and how we can manage content easily. And it's quick, it's fast, it's snappy. And we said, OK, let's try. Let's go. Let's do it. So we did.

[Dato made that simple](https://www.datocms.com/features/developer-experience.md).

(Video content)

Want to change a photo on a trail? Do it in Dato, it just works. Need to update an image on the fishing spot? Fast, quick, easy.

To keep the experience feeling instant, they implemented a granular caching strategy with Next.js paired with [webhooks from Dato](https://www.datocms.com/docs/general-concepts/webhooks.md). The cache invalidates and regenerates automatically when content changes. Static performance with the [flexibility of a dynamic CMS](https://www.datocms.com/features/dynamic-layouts.md).

For a team putting content in themselves, that matters. Creating a new page, adding videos or photos to project details, it all just works without fighting the CMS.

---

Looking at this website come to life was really fun for us too, and for the San Rita crew, the moment that made the whole project click happened on a call. Alexis and Julien had been talking about this idea for months. Sketching maps. Planning the concept. Then Sebastien said he could build it, and they told him to just go for it.

> At some point we ended up meeting with Sebastien. He told us, yeah, I can do it. We're like, okay, so we'll trust you. Just do it. And I remember being on the first call where he showed us the map and where we could interact with it. And I was with Julien on the other side and we're not talking. We were just listening to Sebastien doing his demo and we were looking at each other like, this is fucking insane. Like we made it, it's gonna happen. It's gonna be a real website.

The vision they had in their heads is now running live on the web.

---

# On mirroring bold design in structured content

Source [casual-chats]: https://www.datocms.com/casual-chats/on-mirroring-bold-design-in-structured-content.md

## Customer Stories


(Image content)

In conversation with Mees Rutten (Co-founder and Creative Developer at Merlin Studio)

[Merlin](https://www.datocms.com/partners/merlin-studio.md) is a small Amsterdam-based code boutique that ships high-end digital experiences. And when we say high end, we mean HIGH end.

Check out some of the work they've done with Dato for some context on why we were so obsessed with talking about their visual direction with them:

-   [WØRKS](https://works.studio/) – in collaboration with WØRKS,
-   [Their own website](https://merlin.studio/) on Merlin,
    
-   [SKKY](https://skky.com/) – in collaboration with WØRKS,
-   [erthos](https://planeterthos.com/)® – in collaboration with WØRKS,
    
-   [Mike Schwartz](https://mikeschwartz.work/) – in collaboration with WØRKS,
-   [Summoner](https://summoner.studio/) – in collaboration with Summoner,
    
-   [Lessen voor het leven](https://lessenvoorhetleven.com/) - in collaboration with N=5, and
-   [FANDOM](https://fandomalbum.io/)
    

Their work looks nothing like templates because the team starts by mapping the intended look and feel into a clean data model, then builds the UI to match. The result is visual freedom without chaos in the CMS.

(Video content)

They don't run discovery with a checklist. They sit with the client or the design partner and listen for signals.

Has the team shipped work like this before?

Is it one site or a multi-site?

Are 3D interactions on the table?

How much creative license exists?

Those answers shape both the front end and the content model from day one.

And when they have those answers - they tend to start thinking of how the schema needs to be shaped to match the story. In several cases, those thoughts have led to them kicking off a project with DatoCMS, because, in Mees' words 👇

## "Bare bones, but structured"

Merlin’s modeling rule is simple. Mirror the design in data and [stay modular](https://www.datocms.com/docs/content-modelling/modular-content.md) wherever repetition appears. When a lot of pages share the same visual building blocks but in different orders, they model blocks that can be rearranged safely. That lets editors [add content or swap sections without breaking layout logic](https://www.datocms.com/features/dynamic-layouts.md), and it lets the devs stress-test variants by shuffling components before launch.

> We try to mimic the design in data as well as in how it’s positioned

This is why they prefer working with Dato on these visual projects. His experience with multiple CMS has been that they tend to be restrictive by overwhelming you with dashboards, and cards, and clutter.

(Video content)

Dato, on the other hand, is "bare bones but structured", where you start off with a blank canvas and then build your content views based on your schema and added plugins.

> Because Dato is so barebones, we’re able to match the mental model of the design to the layout in the CMS

Speaking of the [schema, the simplicity of the implementation](https://www.datocms.com/features/schema-builder.md) means it stays readable for both humans involved. Editors should not hunt through sidebars to change one string. Developers should not guess what a field does. If a page needs technical toggles, they label them clearly and fence them off. “Sometimes we have a page where we just have settings for a launch date or a button to remove the zero state. We mark it with *please do not touch if you are the client*,” he chuckles. The UX test however, is blunt. If the CMS feels intuitive, the design is right.

Handovers are hands-on. After build, they clean up labels and help text, then walk editors through the model in a live session and record it for later. They also reach for small plugins when they unlock a real task.

A good example is [plugin](https://www.datocms.com/features/plugins.md) they built which is an audiobook helper that inspects uploaded files, extracts chapter info, visualizes it for editors, and feeds that data back into the site.

## Fragments, frameworks, and solid foundations

OK but with all this visual storytelling and animation-rich design, how are their projects not taking a million years to load on the client side?

On the stack side of things, Mees keeps things pragmatic. [Many projects run on Next.js](https://www.datocms.com/cms/nextjs-cms.md). The team keeps a small GraphQL workflow with reusable fragments, so media, SEO, and other common selections are consistent across screens. Defaults like site-wide SEO come from a central settings record. [Svelte and SvelteKit](https://www.datocms.com/cms/svelte-cms.md) are also in rotation when the work leans heavy on motion or 3D. The reason for this is control. Svelte lets them sit closer to raw HTML, CSS, and JavaScript which helps when performance and accessibility need a tighter grip.

(Video content)

They keep rich-text freedom in check as well to ensure performance is optimized. Editors can add links or bold (standard formatting options), but, for example, they avoid allowing full HTML that turns a field into a page inside a page. That discipline protects rendering performance and prevents design drift.

On the assets side of things they built a nifty video transformer that pulls media, re-encodes to modern codecs like H.265 and VP9, and ships smaller files for users without manual work per clip. DatoCMS slots in well here because it makes bulk processing and delivery straightforward.

A standout example of how they combine their pragmatism with aesthetics is a WebAR experience they did for [Dior for the Garden of Dreams](https://merlin.studio/work/dior-garden-of-dreams) project. There was no page tree. The experience was a linear story composed of models, pop-ups for things like camera permission, and interstitials with text, images, or video.

Merlin solved it in data with clear naming and types for each state in the flow. That content model made the non-page world legible for editors and also scaled to 15 countries and 15 languages during rollout. “We figured out a way to name things really obviously. Promotional pop-up for this moment. Initial pop-up for the cookie banner. We were able to put all those things in data and it was very clear,” Mees says.

## Why DatoCMS?

This is where the whole "bare bones" comment shines.

> Noise is the enemy.

Mees wants one place to own content and a UI that does not encourage unhinged editing. “We really love that Dato allows us to create a space that has so little noise that you can almost not do the wrong thing,” Mees says. Text, media, SEO, and per-project toggles live in one model. Editors can update a staging domain, sanity-check changes, then promote to production without a dance through other tools. It is a simple loop that keeps the team focused on shipping rather than shepherding.

(Video content)

The approach scales down to small sites and up to complex interactive work because the contracts are stable. Model the design in data. Keep the blocks safe to rearrange. Use fragments to keep queries dry. Keep rich text constrained. Encode assets lean. Choose the framework that gives you the control the visuals need. And always make the editor view reflect the mental model of the final experience.

That last part is why the team records handover sessions and why they invest in little helpers like the audiobook plugin or the video transformer. The fastest support ticket is the one that never gets created because the model was obvious and the media just worked.

Mees' take is not theoretical. It comes from keeping performance budgets while pushing motion and 3D, and from avoiding the slow drift that happens when editors get a canvas they can paint into anything. Give them the right blocks. Keep the schema honest to the design. Keep the site fast. The rest is taste. And pretty damn good taste, too.

---

# On powering offline wayfinding

Source [casual-chats]: https://www.datocms.com/casual-chats/on-powering-offline-wayfinding-for-printemps.md

## Customer Stories


(Image content) (Image content)

In conversation with Ivan Leider (Director of Engineering) and Maximilian Benner (Engineering Lead)

When [Printemps](https://www.printemps.com/), one of France’s most iconic retailers, decided to open its [first US location at One Wall Street in New York](https://us.printemps.com/), they didn’t just want to bring their heritage. They wanted to push the in-store experience forward. That meant rethinking what it could feel like to navigate and explore a physical retail space, and what role digital tools could play in supporting that.

This wasn’t about signage or paper maps. Printemps wanted a wayfinding system that felt integrated into the space itself. One that helped visitors find their way around the store. One that could live on in-store digital screens, flow naturally into a mobile experience, and stay connected to their broader digital ecosystem.

Doesn't sound like your classical Headless CMS use case, does it?

But that's where [L+R](https://levinriegner.com/) coming into the picture changes things.

After having collaborated with Printemps for years, Ivan Leider and Maximilian Benner worked closely with the team to design and build an interactive wayfinding system. A system flexible enough to handle future updates. Simple enough that the Printemps content team could maintain it themselves. And robust enough to connect with other parts of the business like their e-commerce brand directory.

This wasn’t the usual website build. And that’s exactly what made us love everything about it.

## Rethinking wayfinding as content architecture

The goal from day one was to avoid thinking about the wayfinding app as just a navigation tool. It needed to feel connected to the rest of the Printemps digital stack. The same data powering the in-store screens should also feed the QR-based mobile experience. If the brand directory was updated, that change should show up everywhere.

(Image content)

But keeping this kind of content in sync is where a lot of projects stumble. If you’re duplicating content across platforms, you’re setting yourself up for overheads. Updates get missed. Teams waste time. Things fall out of sync.

> The philosophy behind the wayfinding setup was that it should house everything related to the in-store experience — not just maps, but brand directories and other services.

The approach that Ivan and Max took was to model the wayfinding app almost like a database, not a website. Content was structured around the real entities that existed in the store. Brands. Rooms. Amenities. Services.

Each of these existed as their own object.

Each could reference the others where needed.

(Image content)

That meant if a brand moved from one room to another, there was only one place to change it. No chasing down templates. No editing multiple pages. One update in the CMS, and the change was live across every endpoint that used that data.

## Adding DatoCMS into the mix

L+R had used DatoCMS on previous projects, so there was already trust there. But for this project, it wasn’t about familiarity. It was about fit.

(Video content)

For Max, the choice came down to keeping the CMS focused on content, not on layout decisions. The last thing they wanted was a system where content editors could accidentally break the design or introduce inconsistencies. The design was already solved by the UX and creative teams. What the editors needed was a clean, structured way to manage the data that the app would use.

> One of the reasons we like DatoCMS is because we model it as if it were a database. We reason about it in relational terms, not as collections of pages.

DatoCMS hit the sweet spot. It gave the [flexibility to model complex relationships](https://www.datocms.com/docs/content-modelling.md) between brands, locations, and services, but it never pushed into the territory of making design choices. Content editors could update the information they needed without worrying about breaking the app.

That balance between [developer control](https://www.datocms.com/team/best-cms-for-developers.md) and [editorial ease](https://www.datocms.com/team/content-creators.md) was what made DatoCMS feel like the right choice. It stayed out of the way when it needed to but gave just enough power to make the system flexible.

## Modeling the physical space

The physical world rarely maps neatly onto digital structures. Rooms aren’t pages. Brands don’t just sit in folders.

So Max and the team modeled the CMS around the reality of the store itself.

(Image content)

Rooms existed as their own entity. Brands as their own entity. Amenities, services — all with their own records, fields, and relationships.

(Image content)

The relationships between these objects were the glue that held the system together.

(Video content)

The approach was simple on paper, but the impact was huge. Because the data was relational, not page-based, the same dataset could power in-store kiosks, the mobile wayfinding app, and even the Printemps [e-commerce site’s brand directory](https://www.datocms.com/use-cases/ecommerce.md). The CMS wasn’t just managing screens. It became a source of truth for how the store was organized.

## Keeping the stack minimal

Part of what made the project work so smoothly was how deliberately minimal the stack was. The internal team at Printemps was lean. People were wearing multiple hats. Adding unnecessary complexity wasn’t going to help anyone.

The core stack was built around DatoCMS for content, paired with a frontend setup that could [pull data via GraphQL](https://www.datocms.com/docs/content-delivery-api.md) and render across multiple endpoints. Everything was designed to reduce friction.

(Video content)

When it came to map rendering, Max pulled some big brain moves.

They used JS DOM in Node.js to pre-render different map states as static assets. This let them optimize performance while keeping the interactive experience intact. Visitors could see highlighted rooms, brand locations, or specific amenities, all without waiting for the browser to do the heavy lifting.

(Image content)

Instead of generating these maps on the fly, the system pre-baked them based on the content in DatoCMS. That meant faster load times, less client-side work, and a smoother experience across devices.

## Custom previews

One of the standout features of the project was the preview environment. But interestingly, this wasn’t built inside DatoCMS using the common [Web Preview](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) approach. The team wanted previews that would reflect the true resolution and layout of the in-store screens.

> We use DatoCMS fully stock: no plugins, no custom extensions. It provides all the baseline functionality we need without getting too specialized. We built a custom preview environment that mirrors the actual in-store screens at true resolution. Editors can see exactly how their updates will look before they publish.

Rather than trying to force that into the CMS itself, they created a dedicated preview page on the frontend. Editors could view scaled versions of the actual screens, rendered at the correct resolutions, and see exactly how their content would look before publishing.

(Image content)

This approach let them avoid overcomplicating the CMS. They didn’t need extra plugins. They didn’t need to hack around the editorial interface. DatoCMS handled the content. The frontend handled the preview.

It was a clean separation of responsibilities, and it worked.

## CMS experience that actually works

From the beginning, the team took what they called a bottom-up approach to the content model. Instead of throwing in every possible feature or configuration, they started with the bare minimum required. If the content team at Printemps needed more flexibility, they could always add it later.

That helped avoid the classic CMS problem of overwhelming editors with too many options. The system only exposed the controls that were genuinely useful. Editors didn’t need to worry about layout decisions or frontend configurations. Their job was to keep the content accurate and up to date. The app would handle the rest.

> If a brand moves locations in the store, editors just change that in the brand collection and the update propagates everywhere automatically — across screens, mobile, and wherever else it’s surfaced.

This also made onboarding smoother. Even for editors who weren’t familiar with headless CMS workflows, the setup was intuitive. If something in the store changed, they knew exactly where to go to update it. There was no confusion about what data lived where.

Throughout the build, the focus stayed on performance and reliability. The map pre-rendering handled the biggest potential bottleneck right at the source. The CMS setup was lean, relying on DatoCMS’s native features without unnecessary overhead.

(Video content)

Ivan pointed out that one of the reasons they trusted DatoCMS for this project was because they didn’t have to think twice about security, performance, or API stability. If they’d needed to solve those problems manually, it would have added significant overhead. Instead, DatoCMS gave them a solid foundation to build on without getting in the way.

The architecture was also flexible enough to support future growth. The same data that powered the wayfinding app could easily feed other systems like ecommerce integrations, additional touchpoints, and future store expansions.

## This build hits different

For Max and Ivan, what stood out about this project wasn’t just the technical stack. It was how well the architecture fit the problem.

The CMS wasn’t treated as an afterthought or a place to dump content. It was the backbone of the system. The data modeling respected the reality of the physical space. The editorial experience respected the needs of the people managing that space.

Everything about the implementation favored simplicity and clarity. No unnecessary abstractions. No overengineered solutions. Just a clear path from content to experience.

(Image content)

In the end, the Printemps wayfinding app works because it’s designed around the right questions. How do you keep content in sync across channels? How do you make updates easy for editors without putting too much power in the wrong places? How do you structure a CMS to reflect a physical space, not just a digital one?

> The way we’ve structured the CMS means it’s ready to support other integrations down the line, whether that’s e-commerce or additional in-store features.

For L+R, the answer was to treat content as data. To focus on structure over layout. To keep the stack minimal, but flexible enough to grow.

DatoCMS gave them the tools to do that. But it was the architecture and the decisions around that tooling that made the project shine.

---

# On unlocking unique use cases with Java and DatoCMS

Source [casual-chats]: https://www.datocms.com/casual-chats/on-unlocking-unique-use-cases-with-java-and-datocms.md

## Customer Stories


(Image content)

In conversation with Lorenzo De Francesco (CTO)

[Azimut Marketplace](https://azimutmarketplace.it/) runs a modular financial services platform where partners plug in at different depths, from full API integrations to forms with back office workflows, they're working with some pretty cool stuff, and not just the usual Jamstackey Headless CMS use cases.

We sat down to catch up with Lorenzo, their CTO, who's been integrating DatoCMS to do a lot more than just generating frontends.

Lorenzo’s first order of business as CTO was getting costs and operational risk down. They trialed self-hosting, then chose DatoCMS for reliability and because they did not want to own CMS infrastructure at all. The [migration off their old vendor was scripted and fast](https://www.datocms.com/product-updates/new-contentful-importer.md), then the team rewrote their APIs against GraphQL. The punchline: uptime has been solid, and the monthly bill dropped dramatically.

> It was a great deal because I cut the cost of more than 95%. To be honest.

They did not stop at pages. Editors now ship landing pages through a “builder” made of roughly fifty blocks. Idea to production can be hours. The same structure was cloned for their Spain rollout. That speed has changed how they work and how often content ships.

BUT.

They also didn't just stop at websites. Behind the doors, they've been working with a pretty nifty [Java client for DatoCMS](https://github.com/Lory1990/java-datocms) that lets them run CRM operations from the CMS itself.

## Into the backend

Ok, before getting into the nitty-gritties. The TLDR here is that the team at Azimut Marketplace uses the CMS to create emails that are served to their users via a Java client for deliverability rather than using something like Mailchimp. They also have the possibility to localize the content for their targeting, and how the same logic applies for communication notifications.

The move was simple in concept. Treat backend messages like content. Put the mutable parts in Dato, keep the envelope and delivery in code, and fetch the right localized body at send time. Lorenzo explains the motivation without ceremony. “And of course, back-end sends email. And we do not want to do a deploy every time the marketing department or the content department says, OK, I have a new idea. I want to change this email.”

(Video content)

The interesting work though is on the server side - backend systems also talk to users and need localized strings that change often. The team moved email bodies and other server-side copy into Dato so marketing can edit without a deploy. The HTML body lives in Dato. A standard template lives in code. The service stitches them and hands the result to a cloud mail sender. The CRM keeps being a CRM. Dato holds the words.

> And of course, back-end sends email. And we don't want to do a deploy every time the marketing department or the content department says, OK, I have a new idea. I want to change this email.

Concretely, delivery is "boring" by design. They use the mail service that exists in their cloud, and the Dato integration is strictly for content. That separation keeps the architecture simple, cheap, and testable. Not to mention extremely flexible, for when new ideas pop up, something we discuss later.

## Why a Java client?

Azimut Marketplace runs Java with [Quarkus](https://quarkus.io/). There was no JVM client for Dato, so they wrote one and open sourced it to [Maven Central](https://central.sonatype.com/artifact/io.github.lory1990/java-datocms). It is intentionally small. The SDK does not invent new concepts. It takes a GraphQL query string, forwards it, gives you a response, and gets out of the way. Types are your job, which is exactly how most Java teams want it for backend services.

(Video content)

> You install it from Maven central. So it is a Maven install or you can go to Maven central, hit DatoCMS Java client or DatoCMS Java SDK. Take the pom.xml code or even Gradle code. You write it in your code base and then you install the dependency. Done.

From there you instantiate `sdk-client`, inject the API key, and call one of the “get data from datocms” methods with a project and an arbitrary GraphQL query. The only discipline you need is to define your response types and regenerate them when schemas change. Localization is expressed in the query, not hidden behind magic.

That rawness is on purpose.

## How its being used

Push notifications are the canonical case for server-side localization. Devices render what they are given. There is no translation stage on the client. That means you fetch the right language from Dato before you call Apple or Google. Technically, should they expand the communication range, they'd have the same story for SMS. Same story for voice prompts in multi-factor flows where a provider does text-to-speech. All of these become safer and faster if they read from the same content source as the website.

Honestly, its' a pretty zippy and nifty service as it is, but with the scope for expanding it based off of such a nice simple package, the potential use-cases are kinda dope.

## DX in practice

The DX is about as minimal as you can make it.

Add a dependency, create a client, pass a query, map the response.

If the schema changes, regenerate your types where it matters.

If you need locale variants, put them in the query.

If you need different backends to share a content shape, share the fragment.

The result is a backend that can speak to users across channels without a parade of tools, and an editing flow that never pings engineering for copy changes.

Azimut Marketplace built and released the client because it solved their problem and because they wanted it to be useful beyond their walls. They plan to evolve it as their own needs evolve, but only in ways that stay generic enough for the community. That constraint keeps the SDK lean and stops it from turning into a product of its own.

---

# On shipping rapidly from one project to the Nuxt

Source [casual-chats]: https://www.datocms.com/casual-chats/on-shipping-rapidly-from-one-project-to-the-nuxt.md

## Customer Stories


(Image content)

In conversation with Andrija Šulić (Co-Founder and Product Lead)

[Trampolin](https://www.datocms.com/partners/trampolin.md) is a creative studio from Europe that likes to do things properly.

They build brands, interactive experiences, and complex platforms. What ties everything together is their approach. Every project starts with a consistent technical foundation, designed to give the team full control, and enough flexibility to push the design wherever they want to go.

They call that foundation Boiler: A Nuxt/Dato boilerplate that's prepared to scale for any project that comes their way.

(Video content)

We sat down with Andrija and Dragan to talk about how they’ve scaled this setup across a range of use-cases like cultural institutions, radio platforms, animated storefronts, and fast-moving editorial teams, all without reinventing the wheel.

## Sticking with what works

Boiler powers all Trampolin projects. It includes the essentials: a ready-to-use UI component library, internationalization, DatoCMS schema integration, and a deploy-ready setup. For the team, it is not just about saving time. It is about avoiding chaos from years of experience.

> We’ve been using the same stack for everything, which gives us kind of our super Swiss army knife that we can use on each and every web project.

[Nuxt](https://www.datocms.com/docs/nuxt.md) gives them a full-stack framework that scales across use cases, from static sites and hybrid apps to web platforms with API layers, e-commerce, or realtime components. DatoCMS handles the structured content, and its clean editor experience means clients understand what they are working with almost immediately, minimising their average onboarding time to about 2 hours.

Boiler is consistent across all projects - [internationalization](https://www.datocms.com/docs/general-concepts/localization.md), Lighthouse focus, consistent naming conventions for fields... That consistency lets the team move quickly while still keeping flexibility and control. Once deployed, it is extended as needed, and more technologies and frameworks are added into the mix depending on the project needs. The content structure stays predictable. The build process stays stable. The project scope can grow without forcing a rebuild. It just works.

(Video content)

Boiler is so well-structured as their foundation, that they're able to easily extend it to other technologies and frameworks as needed, using modules they've got in place to plug-and-play other tools into their stack depending on the project.

## Schema-first

[Schema](https://www.datocms.com/features/schema-builder.md) comes first. Trampolin aligns their design and development workflows with the structure of the CMS. Components in the frontend mirror sections in DatoCMS. Naming conventions are fixed. Blocks are modular and controlled. Every developer on the team kinda already knows how a new project will be structured before they even open the repo.

> We can have one person jump into a project that’s been running for two months and immediately start working, without any onboarding.

Their content models include [built-in validations, field constraints](https://www.datocms.com/features/data-integrity.md), and reusable structures for SEO and performance best practices. Editors are guided through structured flows, and can only create layouts that match the original design logic, ensuring there's nothing breakable by accident.

(Video content)

The schema doesn’t just support the frontend. It also helps enforce standards for accessibility, responsiveness, and structured metadata.

> If the schema breaks down, everything above it becomes harder to manage.

That is why they treat it as the core of every build, not a side task to clean up later.

## Creativity 🤝 Performance

One of the more surprising parts of their projects is that many of their builds are visual and interaction-heavy, but still deliver clean Core Web Vitals and near-perfect Lighthouse scores. This is not a happy accident.

(Video content)

On projects heavy on visuals and animations, like their [Hitradio Center](https://www.datocms.com/partners/trampolin/showcase/hitradio-center.md) one, boiler is optimized for performance from the start. It includes default lazy loading strategies, uses GPU-accelerated CSS transforms, avoids layout shifts, and limits JavaScript payloads with tight control over third-party libs. Performance tracking is integrated into the dev process, not added later just for "handling pretty effects".

> We check performance scores constantly. It’s something that was pushed into our dev team early, and now it’s part of how we work from the start.

For example, for projects like SpaceKart, they leaned on GSAP and Three.js for animation, but made trade-offs early to avoid heavy scripts where unnecessary.

(Video content)

For [Beletrina Digital](https://www.datocms.com/case-studies/beletrina.md), they integrated custom video and audio players alongside static editorial layouts and B2B sub-portals, all from a single codebase.

(Video content)

The result looks diverse on the surface, but under the hood it remains stable and fast.

They have also built modules into Boiler for Unity integration, real-time audio streams, and 3D model embedding via DatoCMS. None of this happens without tight planning around performance budgets. Every interaction is tested before launch.

Honestly, its all pretty damn dope!

## Let's get back to us 💅

Of course we've gotta make it all about us somehow though 😅

The end goal for Trampolin is a CMS their clients don't have to fight with. [Onboarding is simple](https://www.datocms.com/features/editor-experience.md). Most handovers happen over a single recorded session where two hours is usually enough.

The only pain point was the wait time for rebuilds after content changes. So they solved that too. The Boiler starter now includes [real-time previews](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md), allowing editors to see their changes live on staging without needing a full deploy.

> It’s like editing any other website. It refreshes instantly, and they don’t have to wait for the site to build.

The CMS itself has never been a blocker. Trampolin’s team consistently returns to DatoCMS because it balances flexibility with clarity. It is fast to work in, doesn’t overwhelm editors, and scales well with project complexity.

> Nobody has ever said anything bad about the CMS. And that’s a confirmation that we made the right choice.

Features like [GraphQL types](https://www.datocms.com/features/headless-cms-graphql.md), built-in [image optimization](https://www.datocms.com/features/images-api.md), and multiple environments make [development smoother](https://www.datocms.com/features/developer-experience.md). The Appearance tab lets them brand the experience for each client. The [Plugin SDK](https://www.datocms.com/features/plugins.md) gives them control when any specific new features are needed.

---

# On modernizing with a world-class stack

Source [casual-chats]: https://www.datocms.com/casual-chats/bejamas-and-van-raam.md

## Customer Stories


(Image content) (Image content) (Image content)

In conversation with Gerald Martinez (Senior Developer) , Tessa Kolkman-Landewers (Online Marketing) and Luuk Heersink (Online Marketing)

[Van Raam](https://www.vanraam.com/en-gb), a leading manufacturer of customized bikes in the Netherlands, collaborated with our partner [Bejamas](https://bejamas.io/), to revamp their website. Moving from a legacy setup, the new website launched with a focus on significantly improving user experience, speed, and scalability.

The previous website struggled with slow loading times and limited scalability, affecting the team’s ability to effectively reach and serve their content, leading to the need for a more robust and flexible solution. Specifically, the reasons that led to them collaborating with Bejamas were slow loading times (6-7s in the US!), limited features, poor UX, and a general lack of scalability.

After evaluating a few options, Gerald and the team eventually settled on DatoCMS, for a few particular reasons that stood out in the evaluation phase.

[**Blocks**](https://www.datocms.com/docs/content-modelling/blocks.md): Blocks allowed for the creation of reusable, dynamic content templates, allowing for easy updates and consistency across the site.

[**Localization**](https://www.datocms.com/features/headless-cms-multi-language.md): With Van Raam’s website constantly being updated in English, Dutch, French, and German, DatoCMS’s simple approach to i18n allowed for localization workflows that the team could easily adapt to.

[**Scalability**](https://www.datocms.com/features/worldwide-cdn.md): The CMS’s scalable architecture supported Van Raam’s expanding content needs and future growth plans, since the previous Legacy CMS was heavily restrictive.

[**Editor focused UX**](https://www.datocms.com/user-guides.md): The intuitive and easy-to-use UI improved the overall experience for the editorial team, with a very short learning curve.

## Implementation and Process

When Gerald and Bejamas first started, they looked into various CMS options, including other Headless ones. They quickly found that the alternative on the shortlist had some limitations with its content modeling, so they needed a better fit, which ended up being yours truly 💅

(Video content)

In the design and development phase, the Bejamas team created detailed wireframes with a mobile-first approach to ensure the site would perform well on all devices. They designed specific templates for different page types and used [Next.js](https://www.datocms.com/cms/nextjs-cms.md) to integrate DatoCMS, building a dynamic and responsive website. Algolia was also implemented for search functionality.

> Our main stack with Next.js integrated perfectly with DatoCMS, making it **easy for editors to preview content** before publishing.

For the content migration and integration, Bejamas developed custom scripts to seamlessly move existing content into the new CMS. They connected the site with various third-party services using webhooks, which allowed for instant search updates and automatic redirects without issues.

And finally, to enhance the editor UX, the team provided a brief onboarding session to familiarize the Van Raam editors with the new system while setting up core plugins like Live Web Previews. The intuitive interface of DatoCMS made it easy for them to adapt quickly.

## Getting Hands On

Once all the implementation was done and the team had a chance to work with DatoCMS for a few months, we tried to understand some of the aspects around the product’s features, and the overall UX that stood out to both sides.

### Key Features

The following features of DatoCMS stood out for both the development and editorial phases:

[**Web Preview Plugin**](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md): This tool allowed editors to preview pages before publishing, ensuring that content was accurate and visually appealing.

> The ability to preview pages before they go live has significantly improved our content workflow. We have a team of eight working daily on content, and the **web preview feature in DatoCMS is crucial** for ensuring everything is perfect before going live.

[**GraphQL Explorer**](https://www.datocms.com/features/headless-cms-graphql.md): Simplified querying and integration with the front-end, making development more efficient and streamlined, especially when testing out queries and complexities before adding them into the main repo.

(Video content)

**Webhooks**: Enabled seamless automation and integration with third-party services, such as Algolia for search, as well as during their content migration phase to reduce manual back and forth.

**Blocks**: Facilitated the creation of dynamic and reusable content blocks, enhancing content management and consistency.

> The **blocks concept in DatoCMS was perfect** for building dynamic templates and landing pages.

**Localization Features**: Made managing content in 4 languages straightforward, allowing for quick and efficient updates across different locales that Van Raam uses (English, Dutch, French, and German).

### User Experience (UX)

The revamped website offered significant improvements in user experience for the content team of 8 at Van Raam:

**Enhanced Speed and Performance**: The use of CDNs and optimized architecture reduced load times drastically, providing a faster browsing experience for end users, which is a core metric for the content team at Van Raam.

**Localization:** With locale-specific validations and a simple UI for changing between languages, the localization workflow at Van Raam is highly intuitive with everyone on the team being able to handle translations with minimal onboarding.

> Switching and **adding content in four languages is really easy**, and we’re very satisfied with the new CMS system.

**Improved Navigation**: The new design and structure made it easier for users to find information, enhancing overall usability.

**Mobile-First Design**: Ensured that the site performed optimally on all devices, catering to the growing number of mobile users, and allowing the team to safely create content knowing how it would appear across platforms.

### Developer Experience

From the developer's perspective, DatoCMS provided several benefits according to Gerald.

(Video content)

[**Comprehensive Documentation**](https://datocms.com/docs): Detailed and clear documentation facilitated a smooth implementation and customization process.

**Easy Integration**: Connecting with other services and APIs was straightforward, enhancing the overall functionality of the site.

**Powerful Features**: Features like GraphQL querying, webhook integration, and the GraphQL Explorer made development more efficient and less time-consuming.

> The **GraphQL Explorer in DatoCMS is really good** for testing queries before using them in the application.

Looking ahead, both Bejamas and Van Raam have plans to expand the functionality of the site by introducing new page types and elements given how seamless the collaboration process has been for new initiatives. This includes constant optimizations to ensure the site is always performing at it’s optimum best, and exploring new features to implement that enhance the end UX.

With page load times reduced from 6-7 seconds to under 2 seconds, and builds now taking less than 2 minutes, we’re confident that Van Raam’s future website growth is going to continue being mint :chef-kiss:

---

# On seamlessly managing multiple clients

Source [casual-chats]: https://www.datocms.com/casual-chats/fully-studios.md

## Customer Stories


(Image content)

In conversation with Emil Hernqvist (Fullstack Developer)

## About Fully

[Fully Studios](https://fullystudios.se/) is a digital studio based in Sweden, known for their diverse range of projects, from creating websites for beloved companies like [Toca Boca](https://tocaboca.com/), to animations and game development for titles like [Planet of Lana](https://wishfullystudios.com/). With an in-house competence that spans across the spectrum using frameworks like Next.js, Unity, Three.js, and PixiJS, Fully Studios also collaborates closely with sibling companies involved in full-scale video game development.

Focusing on the web development side, the crew is relatively small, with a core team of five web development specialists, They focus on leveraging modern tools like Headless CMS to improve their workflow and deliver better SEO and user experiences for their clients’ websites. Their approach is to build lasting solutions that not only meet current technical standards but are also scalable for future needs.

We caught up with Emil Hernqvist, Fullstack Dev at Fully Studios, to talk about their experience on working with us for multiple projects from the perspective of an agency partner.

## TLDR

-   Fully Sudios prioritize internationalization, UX, modularity, and robust GraphQL API integrations when choosing a headless CMS.
-   Dato stood out for its localization, media handling, validation, and extensibility, which enhance both user experience (UX) and developer experience (DX) according to Fully.
    
-   Fully Studios uses a modern stack including GraphQL and Next.js to streamline development, resulting in faster implementation and more efficient project workflows - a stack that works flawlessly in integration with DatoCMS.
    

## Considerations to choose a Headless CMS

When selecting a headless CMS for their projects, Fully usually go over several key criteria.

Internationalization and localization are often at the top of that list, given they’re based in Europe and work largely with European clients. They also need a CMS that’s seamless, simple, and understandable for non-technical users, given a majority of their clients are in the transitory phase from legacy CMS like Drupal or WordPress. They also appreciate the ability to create custom plugins to better customize the overall extensibility of the platform without being restricted by the limitations of the CMS, which essentially contributes to their next consideration of modularity and flexibility.

(Video content)

Internally, however, there’s one more consideration, and that’s GraphQL. According to Emil, GraphQL just “makes sense” for a Headless CMS, so naturally CMS with a robust GraphQL API helps to facilitate better querying and integration with their existing workflows.

After working with several CMS, these criteria were quite comfortably met by DatoCMS, especially with a fine balance between the UX and DX, making it one of the (its’ cool, we don’t judge 😅) preferred CMS in Fully’s stack when kicking off new projects.

## Where DatoCMS stands out

We’d already established that Dato was within their stack due to it’s intuitiveness and GraphQL APIs, but let’s get a bit more specific on some of the real feature usage and tangible benefits that make Dato a breeze to work with for Fully.

**Localization**: The seamless handling of multiple locales was a major benefit. The ability to create new records from base languages and the visual errors for incomplete fields ensured a smooth localization process. This was a particularly core consideration given the necessity for a strong i18n in Europe.

**Media Library**: The media library and its strong integration with imgix’s API capabilities like its tagging system and focal point functionality, made managing images straightforward. Editors found the focal point feature particularly useful and intuitive for responsive designs.

**Validation**: Robust validation mechanisms prevented common errors and ensured data consistency.

(Video content)

The ability to add help texts to guide users further enhanced the editing experience.

**Extensibility**: The plugin ecosystem and ability to have plugins in multiple locations allowed the team to extend DatoCMS's functionality as needed, addressing specific project requirements without extensive custom development.

The usage of those features along with some best practices from Fully’s side also led to some pretty cool overall improvements:

**Performance**: Websites built with DatoCMS were faster and more responsive, contributing to better user engagement and SEO performance.

**User Satisfaction**: Clients appreciated the intuitive interface and the ease of managing content across multiple locales. The clear validation messages and help texts reduced the learning curve for non-technical users. According to Emil, “we never really heard back with any issues or questions on how to use Dato”, which we’re taking as a big W.

**Development Efficiency**: The integration of GraphQL definitely sped up the development process. Also, Emil’s a fan of Next.js’s app router approach, particularly from a DX standpoint, so having a Content API that played along nicely with Next helped them get set up and running pretty quick.

## OK, but what’s the real-world feedback?

Naturally we could talk about the feature-set and benefits all day, but knowing that Fully have worked with us on more than one project with multiple clients, we wanted to dive into the deets. What’s Emil’s real experience after working with us, and just as importantly, what’re the final users saying when their projects are handed over to them with a new CMS setup?

(Video content)

The intuitive nature of DatoCMS's interface and the effective handling of localization were frequently highlighted in a lot of the feedback. Clients found the media library easy to use and appreciated the ability to manage images without extensive technical knowledge.

### **User Experience (UX)**

The user experience for content editors in DatoCMS was significantly improved compared to previous CMSs.

> We **never really heard back with any issues** or questions on how to use Dato

The separation of locales, combined with clear validation and help texts, made the editing process straightforward. The media library's focal point feature and tagging system further enhanced the UX, enabling editors to manage visual content effectively without technical assistance.

### **Developer Experience (DX)**

For developers, the ability to generate TypeScript types from the GraphQL schema were particularly valuable. These features provided strong typing and auto-completion in development environments, reducing errors and improving efficiency. The plugin ecosystem allowed developers to extend DatoCMS's functionality to meet specific project needs without significant overhead.

(Video content)

And once Fully had a good repeatable process going on to know what clients appreciated, they invested more into setting up their project foundations internally using a framework we're excited to share more about soon, to accelerate implementation times moving forward.

---

# On combining 3D art generation with content creation

Source [casual-chats]: https://www.datocms.com/casual-chats/dreipol-sfg-basel.md

## Customer Stories


(Image content)

In conversation with Jörg Egli (Senior Software Engineer)

## About the project

[Schule für Gestaltung (SFG) Basel](https://www.sfgbasel.ch/), an art school in Basel, Switzerland, partnered with [dreipol](https://www.dreipol.ch/) to [design a new website](https://www.datocms.com/partners/dreipol/showcase/sfg-basel-relaunch.md) that embraced their commitment to innovation and creativity. They required a site that featured interactive 3D elements and allowed editors the flexibility to manage content easily.

(Video content)

A look behind the scenes: Building the 3D space in three.js

Using DatoCMS as the content backbone, dreipol developed a solution that prioritized a seamless user and developer experience, without sacrificing on the heavy customization required on the editorial side for generating custom 3D artworks using rich textures.

We were really fascinated by the granularity of the artworks generated, and how editors could create new textured models on the fly, so we caught up with Jörg from dreipol to chat about their experience with the project.

## TLDR

-   **GraphQL Playground**: dreipol utilized DatoCMS's GraphQL API Explorer to create efficient data queries and incorporated a live preview for real-time content updates.
-   **3D Art Customization**: The CMS enabled editors to control 3D textures and visual parameters directly from within the CMS and get a live preview of what the end results would look like.
    
-   **Plugins**: Conditional controls and pre-built plugins streamlined the editing experience, making DatoCMS accessible to non-technical users.
-   **Developer Experience (DX)**: DatoCMS’s flexible schema management and clean UI empowered frontend developers to manage backend tasks efficiently.
    

## Implementing DatoCMS

From the beginning, dreipol saw DatoCMS as an ideal fit for SFG Basel’s requirements.

In their initial pitch, dreipol built a simple prototype that showcased dynamic 3D content controlled via DatoCMS - a core requirement for the project. This early example highlighted the flexibility and straightforwardness that the editors could expect when working with Dato, and helped secure it as the preferred platform for the project.

> For SFG Basel, we showcased 3D elements where they could dynamically choose textures and colors right from the CMS, which won us the pitch.

On the other hand, DatoCMS’s user-friendly interface and strong DX made it a preferred choice for dreipol's frontend team, who needed to manage both the presentation and content modeling.

The no-code schema setup allowed for a collaborative, iterative approach, with dreipol meeting with SFG Basel fortnightly to adjust content structures and provide guidance. Not only did this help the content team understand the foundations behind the structure, but led to minimal onboarding at project completion since they were quite familiar with the project already.

> DatoCMS is clean, focused, and doesn’t have unnecessary features. It’s just what we need without the bloat.

The team defined specific validation parameters, such as character limits and contextual help texts, to ensure an intuitive editing process with [content integrity](https://www.datocms.com/features/data-integrity.md) in place. This hands-on, flexible approach to schema development allowed dreipol to quickly adjust content structures based on SFG Basel’s evolving needs, keeping the platform manageable and user-friendly.

## Where DatoCMS stood out

We'd already established that the dreipol team were in favour of choosing Dato, but some key features on both the editorial and development sides stood out to reassure them that they were making the right choice.

### GraphQL Explorer

The DatoCMS [GraphQL API Explorer](https://www.datocms.com/features/headless-cms-graphql.md) was a big plus to dreipol's approach.

(Video content)

The CDA playground allowed for pre-documented and efficient data querying, allowing them to break down queries into reusable fragments for different components in a safe sandbox environment and then take it to their repo.

> We’re big fans of the GraphQL API Explorer. The flexibility with GraphQL queries is fantastic. It’s intuitive and helps us easily reuse fragments across different queries.

It also streamlined the process of identifying and fetching specific content attributes, accelerating development and improving both the UX and DX by reducing loading times and optimizing the data flow across the site.

### Plugins and Validations

DatoCMS’s [plugin ecosystem](https://www.datocms.com/marketplace/plugins.md) was another major key in enhancing the editorial experience. dreipol integrated plugins such as the [live preview](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md), which provided a real-time view of content changes, greatly benefiting SFG Basel’s editors.

(Video content)

The content team would often upload new textures and generate artwork for landing pages using those textures, along with some presets for angles, lighting, and other visual configurations on how the end result should look. Having the live previews side-by-side was a huge time saver in letting them actually visualize things before pushing them to staging or production.

Additionally, they employed conditional controls, which dynamically hid or revealed fields based on specific inputs. This modified the interface, making the CMS less cluttered and more intuitive, especially for non-technical editors.

### Customization for 3D Artwork Generation

A standout feature of the new website was the dynamic 3D art generator.

(Video content)

Initially conceptualized during the pitch phase, this capability evolved to allow editors direct control over various 3D visual parameters. The implementation centered on user-friendly inputs within DatoCMS, where editors could upload textures, adjust displacement, control lighting angles, and modify surface properties. This customization capability empowered SFG Basel’s editorial team to bring their creative vision to life, enabling on-the-fly adjustments to interactive 3D elements without developer intervention.

### Integrated Search

DatoCMS’s built-in search functionality, with its simple setup and configuration, further helped dreipol to include options like fuzzy matching and they found the API responsive enough to handle content search efficiently. The search was easy to integrate and required minimal adjustments, providing users quick access to relevant content.

## Balancing User Experience with Developer Experience

Another few points that stood out in our chat was the overall experience for editors and devs. Granted that the editors use the CMS the most beyond a point, but the DX had to be equally strong to setup such a complex project in a straightforward manner.

Here's a few points that Jörg felt really enhanced the overall experience.

### Content Modeling and Migrations

DatoCMS enabled dreipol's frontend team to take an active role in backend configuration and content modeling.

> DatoCMS makes it super easy as a frontend developer to get into content modeling. It's a great developer experience.

This flexibility was especially advantageous as they could make real-time schema adjustments during the regular feedback sessions with SFG Basel. For ongoing content updates, dreipol used DatoCMS’s migration capabilities, allowing them to automate schema changes, such as updating category tags, without requiring any manual intervention whatsoever.

(Video content)

This adaptability ensured that the site could evolve with minimal disruption, an essential feature for a dynamic content-rich site like SFG Basel’s.

### Plugins and Workflows

Real-time previews enabled editors to see changes as they made them, reducing the need for repetitive testing with hits and misses. The combination of the GraphQL API and live previews also improved dreipol's development workflow by allowing developers to visualize the effects of their code changes instantaneously, resulting in a more connected integration between the frontend and backend.

The modular plugin architecture of the CMS allowed them to also expand the CMS’s functionality without sacrificing simplicity. By using plugins for tasks such as conditional field display and live preview integration, they were able to create a purpose-built CMS that met SFG Basel’s specific requirements without editorial complexity.

### Future Expansion

DatoCMS’s feature set and flexibility leaves room for future expansion.

Balancing new features with the ease of being able to implement them straight out of the box without any needs for replatforming gives them the peace of mind that the project can be enhanced if and when needed.

> Seeing that DatoCMS added the media collections feature recently was great. The client had requested it initially, and now they can organize files easily. The new cache tags feature also works really well with Next.js’s revalidate tag, making the caching process smooth and manageable.

dreipol has the option to easily add new functionalities as SFG Basel’s needs evolve, such as additional 3D components or interactive content. By managing ongoing updates with migrations, dreipol can efficiently introduce new features while maintaining content integrity, ensuring that their implementation stays scalable in the future.

---

# On building memorable eCommerce experiences

Source [casual-chats]: https://www.datocms.com/casual-chats/rotate.md

## Customer Stories


(Image content)

In conversation with Chris Harris (Chief Experience Officer)

[Rotate](https://studiorotate.com/)° is a technology studio from London, focused on crafting beautiful, brand-led eCommerce experiences that not only delight customers but drive measurable growth for some pretty cool brands.

Over the past year, they've been focused on scaling their capabilities, especially around personalisation, seamless customer journeys, and integrating and extending powerful, flexible platforms like yours truly, to deliver composable eCommerce experiences for these clients.

We've collaborated on some exciting projects together, such as [Chilly's](https://www.chillys.com/de), [Tracksmith,](https://www.tracksmith.com/gb) and [Wild](https://wearewild.com/), where Rotate° helped them navigate complex eCommerce challenges as they scale, whilst also elevating their digital presence.

So we sat down Chris, Chief Experience Officer at [Rotate](https://studiorotate.com/)°, to dive into a casual chat about our relationship, their approach to eCommerce, what's critical to consider when building eCommerce experiences, and how they work with DatoCMS to create some truly remarkable and memorable online eCommerce experiences for household brands like the Big Green Egg and [Wild](https://www.datocms.com/partners/rotate/showcase/wild.md).

### Going composable in eCommerce

**What have you noticed as crucial elements in a successful eCommerce implementation?**

One of the biggest opportunities we focus on is that the most successful eCommerce implementations start with a clear, customer-first approach. You’ve got to really dig into the current customer journey — understand their touch points, pain points, and behaviours. This is informed by data, whether that’s behavioural (using tools like Hotjar, Contentsquare, MS Clarity) or event-based (GA4, Segment), and also attitudinal (user testing, post-purchase surveys).

With a clear picture of the current and target customer journey, we can start to build out an Information Architecture and UX/UI to deliver on this.

Flexibility and future-proofing are also key; building something scalable that can adapt to new customer needs or market shifts is crucial. This has been particularly pertinent in the past 12 months with the economic backdrop, where brands are looking to maximise their engineering time as best as possible. We are spending more time designing the admin experience with scalability and future-proofing front of mind, to ensure the tools don’t become bloated and complicated which will effect admin performance over the long term.

(Image content)

Finally, collaboration between design, development, and content teams is also fundamental. Aligning all stakeholders early and ensuring there’s a clear communication pipeline makes the entire project smoother, faster, and more aligned with business goals.

We consistently aim to get new features into the hands of end users (both consumers and admins) for quick feedback and iterate solutions as required.

### Repeatedly working with DatoCMS

**As a long-time partner , we’ve collaborated on many projects. What are some of the reasons for this working?**

For us at Rotate°, flexibility and performance are crucial, and DatoCMS really excels in both areas. One key consideration is scalability—DatoCMS can grow alongside our clients’ ambitions, and that’s a big win for us when dealing with high-expectation eCommerce brands from a front-end experience perspective. Each of our clients’ sites are unique to them, and we need technology to enable that.

For our clients, content is a critical element in crafting the shopping experience and Dato allows for that product decoration to happen in a seamless fashion, via an intuitive interface.

The speed at which DatoCMS operates, also plays a pivotal role in delivering fast, smooth experiences for admin users, which is exactly what our clients demand.

**Let’s cover some of the considerations when working with the CMS, and what other tools do you see us playing nicely with usually?**

When we were working on the new website for [Wild](https://www.datocms.com/partners/rotate/showcase/wild.md), the decision to use DatoCMS stemmed from its flexibility and its ability to work seamlessly in a composable architecture. We were looking to expand the stack to include a composable CMS to allow for greater content control of key areas of the front-end such as the subscription builder flow.

(Image content)

Wild also leverages Recharge for customer subscription management, where a few experience functions integrate with DatoCMS well.

For [Tracksmith](https://www.tracksmith.com/), we needed a platform that could handle their content-heavy approach while delivering performance. Tracksmith had a specific need to configure rich storytelling at the product-level, and required a content model which could support that.

(Image content)

Other tools in their stack included Shopify for commerce, [Algolia](https://www.datocms.com/blog/algolia-nextjs-how-to-add-algolia-instantsearch.md) for Search & Merchandising, and Klaviyo for email marketing automation. Similarly Tracksmith have managed to leverage DatoCMS as a pseudo PIM for management of their product catalogue.

**And what feedback do you and your customers typically have on the end results?**

The feedback has been overwhelmingly positive. Clients love the intuitive nature of DatoCMS – its user-friendly interface makes it easy for their teams to manage content independently, without having to rely on developers for every little change.

They also appreciate the speed at which the platform operates; faster load times, combined with the flexibility to scale, really make a noticeable difference in the customer experience.

> DatoCMS strikes the balance between flexibility and ease of use perfectly. It’s a rare combination, and it’s a big part of why we are huge fans of DatoCMS.

One of the standout features for us is DatoCMS’s [modular content approach](https://www.datocms.com/user-guides/content-management/building-pages-and-deep-dive-into-modular-content.md). The ability to build content blocks that can be easily reused or repurposed across multiple pages or landing pages for marketing campaigns gives our clients so much flexibility without requiring any technical intervention. Particularly with our bespoke designed sites where brand consistency is critical. Today, we have shifted from designing sites page by page to a more modular approach that uses flexible yet consistent Design Systems to deliver true brand-led experiences. Dato massively compliments this approach when it comes to the build.

Also, the [webhook integrations](https://www.datocms.com/docs/general-concepts/webhooks.md) are incredibly smooth — they allow us to trigger updates across various parts of the stack effortlessly.

Another positive for us is the collaboration with your support team; they’re always quick to respond and work closely with us to ensure our projects run smoothly. It’s that partnership that really elevates DatoCMS in our eyes.

### The Headless CMS for eCommerce

**Let’s focus on eCommerce, what are some standout reasons that make us an ideal CMS for you?**

In eCommerce, especially with the level of sophistication our clients expect, flexibility is key. DatoCMS’s headless architecture gives us the ability to structure content exactly how we need it, ensuring seamless integration with other parts of the stack, like Shopify, Recharge, or Brightpearl.

For us, the key aspects of DatoCMS which are beneficial to our client projects include:

**Content flexibility** - Dato allows you to [design content structures](https://www.datocms.com/user-guides/the-basics/intro-to-the-schema-builder.md) tailored to all business. Instead of rigid, predefined content types, you can design dynamic, modular blocks that can be reused across various pages or components.

**Scalability -** Dato has an infrastructure that supports growing businesses by providing a global CDN, efficient API's and flexible pricing

**API versatility -** Dato allows [precise data fetching](https://www.datocms.com/docs/content-delivery-api.md) via GraphQL/REST, enabling seamless integration with any frontend or service.

**Localisation** - [Dato supports localisation and internationalisation](https://www.datocms.com/docs/general-concepts/localization.md) by allowing multi-language content, region-specific content models, and easy management of translations, enabling global eCommerce merchants to serve diverse markets effectively.

**Integrations and extensibility** - Dato excels in [integrations and extensibility](https://www.datocms.com/marketplace.md) through its powerful API, webhooks, and plugin system, allowing seamless connections with third-party tools and custom functionalities tailored to business needs.

Its multilingual and multi-regional capabilities also stand out, especially for brands like Tracksmith that have a global audience. And because DatoCMS is API-first, it allows our developers to easily pull in content across platforms while keeping things centralised, which streamlines the entire workflow.

(Video content)

Tracksmith is a good example of how we have used DatoCMS to deliver high-performance, high-impact eCommerce experiences. For Tracksmith, storytelling is at the core of their brand, and with DatoCMS, we were able to create a content management flow that allowed them to deliver rich, engaging stories alongside product. Each product page tells a story and Tracksmith can create this at scale across all lines of product within collections.

Similarly, Tracksmith have the capabilities to curate landing pages for Event and Journal content to showcase their experiential arm of the brand, where they foster a community both on and offline.

---

Check out [some of the work](https://www.datocms.com/partners/rotate.md) that Rotate**°** have been building with DatoCMS. If you're looking to build up or replatform a composable commerce project of your own, [let's chat](https://www.datocms.com/contact.md)!

---

# On going beyond the Jamstack with \`dato-rails\`

Source [casual-chats]: https://www.datocms.com/casual-chats/renuo-and-the-birth-of-dato-rails-for-ruby-on-rails-projects.md

## Customer Stories


(Image content)

In conversation with Alessandro Rodi (Software Engineer and Partner at Renuo AG)

Ruby on Rails is a powerhouse for building web apps, but when it comes to content management, for some reason, Headless CMS just tend to be strung into the same sentence with Jamstackey frameworks like Next, Nuxt, and Svelte. Well, the folks at Renuo aren't too happy about that, and so we caught up with Alessandro to understand more about their work with DatoCMS and Rails.

The typical Rails setup doesn’t come with a built-in CMS approach, and most out-of-the-box solutions feel clunky, outdated, or overly opinionated.

That’s the exact problem [Renuo](https://renuo.ch/), a Zurich-based web agency specializing in Rails, faced. They had multiple Rails projects where clients needed an intuitive way to manage their content. They could either build a CMS from scratch (painful, time-consuming, and not as polished as yours truly 💅) or try to retrofit something built for JS frameworks (which, given many of their projects are in the ruby realm, doesn't make sense).

Instead of settling for either option, Renuo took a different route. They built out [dato-rails](https://github.com/renuo/dato-rails), an open-source Ruby gem that brings DatoCMS’s headless CMS experience seamlessly into the Rails ecosystem.

### A short view back to the past

Renuo first encountered us at Ruby Day in Italy (pour one out for our [now deprecated ruby client](https://github.com/datocms/ruby-datocms-client) 🥃).

Although, ironically, their initial project with Dato wasn’t even in Rails—it was a [Next.js site for a very familiar something-something-hamburger-chain-golden-arches client](https://www.datocms.com/partners/renuo/showcase/mcdonald-s-video-platform.md) who needed a smooth way to manage video content. Alessandro remembered how DatoCMS stood out immediately because of its built-in video handling, structured content blocks, and intuitive editing experience. It solved problems they didn’t even realize were problems until they saw how easily we handled content.

(Video content)

Wait, why're we flexxing ourselves. This isn't about us, anyways, back to the point.

After that first encounter, Renuo started integrating DatoCMS into more projects, including their Rails-based web apps. The question they eventually encountered was: How do we make this work as seamlessly in Rails as it does in JavaScript frameworks like Next.js?

Unlike Jamstack frameworks like Next.js, Nuxt, or Astro—where SSG is a core feature—Rails applications are server-rendered. That meant Renuo had to rethink the way Rails fetched, stored, displayed, and, cached content from DatoCMS.

(Video content)

At first, they hacked together integrations for each new project. It worked, but it wasn’t scalable. Every time a new Rails project needed CMS functionality, they had to manually wire up GraphQL queries, manage caching, and figure out how to preview content for the editors. Meanwhile, developers using Next.js or Vue had official libraries that handled all of this automatically, considering our own packages for React, Next, Nuxt, and Astro are very heavily maintained.

> When I started working with Dato on Ruby on Rails, there was no library to do so. I was jealous of all the libraries available for JavaScript frameworks.

That’s when they decided to stop reinventing the wheel, and came up with `dato-rails`.

### Abstracting the pain away

With multiple projects using DatoCMS in Rails, it made sense to abstract the common logic into a reusable gem. Inspired by the way React and Vue handled content rendering, Alessandro and crew built DatoRails, a library that made working with DatoCMS in Rails as easy as it was in JavaScript frameworks.

(Video content)

> I took a lot of inspiration from the React library—actually, I had it open on the next screen and was basically copying how they did things.

Instead of manually setting up GraphQL queries and parsing responses, developers could now:

-   Use prebuilt components for rendering structured text, images, and other content (inspired by Vue's approach to component handling).
-   Fetch data using a simple Ruby interface instead of manually constructing GraphQL requests.
    
-   Support draft and live preview modes without extra work.
-   Leverage Rails caching mechanisms to serve content blazingly fast without hitting the CMS on every request (more on that later!).
    

For Renuo, it meant less custom boilerplate and a much smoother DX with every new project.

### Looking under the hood

The key to making DatoCMS work efficiently in Rails wasn’t just fetching content—it was optimizing how that content was served.

Since Rails renders pages dynamically, they needed a caching strategy that wouldn’t slow down the app. Instead of rebuilding static pages like Jamstack frameworks do, `dato-rails` hooks into Rails’ native caching mechanisms.

When content is fetched from DatoCMS, it gets stored in Rails’ built-in cache, making subsequent page loads nearly instantaneous. But how do you make sure the cache stays fresh?

Here’s where things get interesting. Instead of just caching individual pieces of content, Renuo built a custom publish endpoint. Whenever content is updated in DatoCMS, it triggers a hook that tells the Rails app to expire its cache and reload fresh content on the next request. While this handles the entire cache as a whole, should any of the projects scale beyond a managable point, Alessandro's pretty confident about implementing [cache tags](https://www.datocms.com/blog/introducing-datocms-cache-tags.md), that'd handle this with more granularity.

The result? Pages render in milliseconds—even though the content is dynamically managed.

While all this is cool for performance, there's a big part we haven't really touched upon at all. How the editors are affected with projects dealing with Rails.

One of the biggest goals of the approach was to bring feature parity with the way DatoCMS and React work via the official packages. If DatoCMS worked flawlessly in Next.js, why shouldn’t it be just as smooth in Rails?

With that in mind, there were 4 core things taken care of to make the usage as seamless as possible.

1.  **GraphQL Query Helpers** to abstract away the complexity of making API calls
    
2.  **Components for common features** like Structured Text, Images, and Videos, to make the content sourcing as alike to the JS packages as possible.
    
3.  **Draft Mode and Live Previews**, for editors to have a seamless UX when using Dato, not sacrificing on any of the easy-to-implement features in the other packages, and
    
4.  **Asset Optimizations** like lazy loading, resizing and format handling, to have parity with the [imgix](https://www.datocms.com/tech-partners/imgix.md) optimizations that Dato offers.
    

Bonus flex on top? Even real-time content updates are supported. Using Rails’ Turbo Streams, editors can edit content in DatoCMS and see the changes update on the frontend instantly.

(Video content)

👆 Dive in as Alessandro shows off everything `dato-rails` can do (while Ronak silently watches and pretends to understand everything that's going on 🫥)

### **Taking the OSS Approach**

While `dato-rails` started as an internal tool for Renuo’s projects, they decided to open-source it for the wider Rails community to use.

Since then, we've also been recommending it to a few partners and devs working with Ruby, and it's been a highly appreciated package. For Rails teams that need strong CMS functionality, DatoCMS paired with Rails offers a clean, modern solution without the baggage of legacy Rails CMS options. Instead of dealing with clunky interfaces and outdated plugins, you get a dope experience that's got native support for all the juicy Dato features.

For now, the gem stays focused on what Renuo actually uses in production. It’s an open-source project, but they’re not chasing feature bloat—every addition has to serve a real need.

However, to wrap things up, a rather valid concern with many OSS packages is on the question of future maintainability. So I guess it's worth looking into how serious Renuo are on the topic of Ruby and on DatoCMS to gauge whether this package is going to be maintained a while from now.

So? How serious are they?

**👇**

> Our own website is built with Ruby on Rails and DatoCMS. We didn’t even use a database—DatoCMS **is** our database.

✌️

---

# On flexibility, extensibility, and \`head-start\`

Source [casual-chats]: https://www.datocms.com/casual-chats/focusing-on-flexibility-extensibility-with-plugins-and-head-start.md

## Customer Stories


(Image content)

In conversation with Jasper Moelker (Co-founder & CTO)

The last thing you ever want is a CMS that gets in your way. That’s been the guiding principle for [De Voorhoede](https://www.voorhoede.nl/nl/), the fine folks from the Netherlands who're the faces behind some incredible projects like [Life Terra](https://www.datocms.com/partners/voorhoede/showcase/life-terra.md), Computed Fields, and Word Counter.

Notice something there? There's plugins. Ooh yeah, these peeps have been CHURNING out super [helpful plugins to extend DatoCMS](https://www.datocms.com/marketplace/plugins.md), and between their community and private ones there's easily 15+ floating around in the wild. And obv, to do things like that, they need a CMS that adapts to their needs, not the other way around.

Which is why, DatoCMS is their go-to choice, not just because of our flexibility, but because of how the CMS fits seamlessly into their workflows. From modular content structures to APIs with a great DX, they have the freedom to build without limitations.

But De Voorhoede doesn’t just stop there. They extend, optimize, and automate everything. That’s where their work on DatoCMS plugins and [`head-start`](https://github.com/voorhoede/head-start) comes in.

So let's dive into the whats and the whys behind what they've been cooking 👇

### Let's make it about us

So what is it about DatoCMS that makes De Voorhoede coming back?

They needed something flexible, API-first, and easy to integrate with any frontend framework. [Textbook definition of a Headless CMS innit](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md). That’s what led them to us.

Unlike traditional CMS platforms (*note: most of their earlier clients are from the WordPress category of CMS*), it didn’t dictate how the frontend should be built. The separation between content management and content delivery was clean. Editors got a powerful yet simple interface, while developers had a [GraphQL-powered backend](https://www.datocms.com/features/headless-cms-graphql.md) that could fit seamlessly into any stack.

(Video content)

One of the biggest reasons De Voorhoede stuck with DatoCMS was the approach to content structuring. Unlike other CMS platforms that impose rigid templates, DatoCMS introduced [modular blocks](https://www.datocms.com/features/dynamic-layouts.md) and [structured text](https://www.datocms.com/features/structured-content-cms.md), which allowed them to model content however they needed.

For projects with highly dynamic layouts, blocks provided a flexible, reusable system, letting editors build pages using predefined components without sacrificing consistency. Structured Text took it even further, making rich content more portable and API-friendly, while still giving editors an intuitive writing experience.

> I think you were one of the first CMSs to offer the modular blocks and quickly following with the structured text. And those two combined are just so powerful to basically build whatever you want. And then of course all the other fields around that are nice.

This combination of developer flexibility and editorial control meant they could build whatever they wanted. Whether they were creating standard marketing pages or complex, data-driven apps, everything adapted to their needs—not the other way around.

(Video content)

The DX also played a big role. The mental model behind DatoCMS felt natural to work with, making it easy to structure content in a way that felt right for both developers and editors. [Image optimization](https://www.datocms.com/features/images-api.md), modular content blocks, and structured text were built-in and "just worked". De Voorhoede didn’t need to waste time hacking together solutions for things that should be standard in a modern CMS.

> What attracted us further is the developer experience. The whole mental model around DatoCMS, the APIs, the SDK—everything just clicks.

Ok but enough about us 💁‍♀️ Back to Jasper.

What really set DatoCMS apart was its extensibility, and that’s where De Voorhoede started pushing the limits, building custom plugins and eventually developing `head-start`, an open-source repo that streamlines the integration between DatoCMS and frontend frameworks. Let's talk about that.

### Let's talk Plugins first

DatoCMS does a lot out of the box, but we also walk a fine balance between default features offered to stay at "just enough" without venturing into "feature bloat". But every project has unique requirements. Which is why we encourage the use of Plugins.

This is where De Voorhoede shines - they built plugins to fill the gaps that every project (or several) needed, tweaking the CMS to offer custom functionalities. Over time, they became one of the most active contributors to the DatoCMS plugin ecosystem, creating both public and private extensions to enhance editor workflows.

(Video content)

Their public plugins solve common challenges like computed fields, SEO optimization, and AI-assisted content generation. One of their (and ours) favorites is the Computed Fields plugin, which functions like Vue’s computed props, allowing certain fields to generate dynamic values based on existing content.

> What I personally love is our computed fields plugin. It’s like computed props in Vue - it lets us dynamically generate values that the API doesn’t store by default.

But some of the most powerful work happens behind the scenes with private plugins. For Life Terra, a platform that tracks tree planting, they built a Mapbox-based plugin that lets editors draw regions directly inside DatoCMS. Instead of requiring a separate admin tool, they embedded this functionality into the CMS itself, letting content editors manage geolocation data without ever leaving their content workflow.

> One of the coolest private plugins we built was for Life Terra. Editors could draw a region in the CMS using Mapbox, defining where trees could be planted. That data gets saved as JSON and used in the app.

They also built an AI-powered content importer that scrapes content from external sources and auto-fills fields in the CMS. Instead of manually copying and pasting, editors can simply paste a URL, click a button, and have key data extracted instantly.

> Combining AI with DatoCMS has been a game changer. Instead of manually entering content, we built a plugin where an editor pastes a URL, and AI extracts and structures the content into the right fields.

The motivation behind all this work is simple: make the editor experience seamless and reduce friction in content management. If something takes multiple steps when it could take one, it’s a problem worth solving for them.

Honestly tho, we could go on about how useful these plugins are, but if you're working on projects and need the CMS to do just that little bit more, check out the stuff they've published!

But what good is a fully beefed up CMS if they've to spend hours replicating the work they do over and over again for each client right? Jasper's got a solution for that too. `head-start`. It's an OSS approach to whipping up new projects that rely on DatoCMS, connecting to any frontend framework to speed up the dev process. So let's dive into that.

### With our powers combined

After working on dozens of projects with DatoCMS, De Voorhoede realized that the first few weeks of every project looked the same. The same foundational setup—configuring SEO, localization, structured content, and routing—had to be repeated over and over.

Instead of reinventing the wheel each time, they built `head-start`, an open-source repo that connects DatoCMS to frontend frameworks in a structured and repeatable way.

(Video content)

With `head-start`, devs get a pre-configured environment with best practices already in place. SEO, routing, internationalization, and content models are all set up automatically, so teams can jump straight into building unique features instead of handling boilerplate.

> With `head-start`, we can generate a new project, run a script, and instantly have a structured setup with DatoCMS and a frontend framework of our choice.

One of the best parts of `head-start` is that it’s framework-agnostic. While Next.js and Vercel are a common pairing for DatoCMS projects, De Voorhoede has leaned towards Astro and Cloudflare Pages because of their performance benefits and closer adherence to web standards. But they've kept the package to be relatively agnostic, letting you work with any JS SPA framework, giving you the flexibility to choose what works best for you.

(Video content)

Another sweet feature is [preview mode](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md). While Next.js offers a built-in solution for previewing content (+ [Vercel Content Link](https://www.datocms.com/docs/content-link/how-to-use-content-link.md) when paired with Vercel), Cloudflare required a touch more work. Instead of trying to force static regeneration for previews, De Voorhoede built a separate deployment branch that renders pages dynamically, making preview mode seamless for editors.

At the end, for De Voorhoede, a good CMS setup isn’t just about content management—it’s about making life easier for both editors and developers.

> After working on so many DatoCMS projects, we noticed that 80% of the setup was the same—SEO, routing, localization, structured content. That’s why we built `head-start`.

Editors should be able to jump in and start using the CMS without requiring a training session. Instead of cluttering the interface with unnecessary complexity, they focus on creating a natural editing experience. Inline documentation, tooltips, and well-structured content models eliminate the need for long user manuals.

> Clients always ask, ‘How long is the CMS training?’ Our answer is ‘None.’ If the CMS is well-structured, it should be intuitive from day one.

For devs, `head-start` removes a huge amount of setup work. The repo includes prebuilt GraphQL queries, SEO-ready components, and localization logic, so they don’t have to set it up manually. They’ve even integrated PLOP.js that lets developers auto-generate new components and GraphQL fragments with a single command.

> For developers, we added automation to `head-start`, like PLOP.js, which generates GraphQL queries and UI components with a single command.

Seriously, its' pretty damn cool.

### What's coming up?

As with any OSS project, maintenance is always a talking point. maintaining `head-start` is a balance. De Voorhoede is realistic about what they can maintain long-term, so they focus on features they actively use in production.

Right now, their short-term roadmap includes improving content collections and tagging, making it easier to filter and organize large amounts of content. They’re also working on AstroDB integrations, ensuring that the content layer stays perfectly in sync with the frontend.

Longer-term, they see `head-start` evolving beyond just websites. They’re exploring ways to make it work for app development, integrating with databases for handling more complex relational data. The goal is to create a setup where DatoCMS handles structured content, while a separate database manages user-generated content and application data.

The TLDR on `head-start` really shows how they handle CMS projects strategically instead of reactively, Instead of treating every project as a unique challenge, they’ve built repeatable solutions that make content management faster, smoother, and more scalable, and easier to tweak to the bespoke little quirks that each project requires.

And because they openly share their tools, other teams don’t have to start from scratch. Whether you use the entire `head-start` setup or just borrow individual ideas, there’s no need to reinvent the wheel.

---

# On keeping things simple because static ain't dead

Source [casual-chats]: https://www.datocms.com/casual-chats/on-keeping-things-simple-because-static-ain-t-dead.md

## Customer Stories


(Image content)

In conversation with Virgil Ierubino (Creative Director)

Virgil Ierubino has been building websites since the early days of the internet (since he was 10!). His agency, [Convincible](https://www.datocms.com/partners/convincible.md) [Media](https://www.datocms.com/partners/convincible.md), unlike many chasing the latest frontend trends, focuses on the simplest approach for client projects. For him, web development isn’t about stacking the most cutting-edge tools.

It’s about choosing the right tool for the job.

For most of his projects, that means working with Jekyll as an SSG and DatoCMS as the headless CMS. Together, they allow him to build fast, secure, and maintainable websites that avoid unnecessary complexity.

(Video content)

His philosophy is simple: if a website doesn’t need a database or complex JS frameworks, why add them? Most of his client websites are fundamentally about presenting text and images. They don’t require SSR, hydration, or reactivity. Adding more moving parts only increases the risk of things breaking down the road.

> Most client sites don’t need complex JavaScript frameworks. They just need to present text and images in different layouts. Why overcomplicate it?

Now, for sure many websites need real-time updates, personalization, or complex interactivity, but for the majority of his clients, static is the right solution. The key is knowing when simplicity is a feature, not a limitation.

So we jumped on a call to chat about his philosophies behind that approach, some concerns on whether or not "Static Is Dead", and got his 2p (*since he's British* 🫖) on where he thinks Static is headed.

### Let's talk Jekyll

I'll admit, it's been a whiiiile since I heard about Jekyll. With everyone seemingly defaulting to [Next](https://www.datocms.com/docs/next-js.md), [Nuxt](https://www.datocms.com/docs/nuxt.md), [Astro](https://www.datocms.com/docs/astro.md), and [Svelte](https://www.datocms.com/docs/svelte.md) in my bubble lately, it was refreshing to chat about this and sprinkle in some Docusaurus, 11ty, and Hugo.

Anyways.

Virgil remains committed to Jekyll. And it’s not about nostalgia, it’s about control. Why?

Jekyll doesn’t force a specific architecture or development style. It doesn’t come with a bunch of opinions or predefined components. It’s purely a build-time tool, with no JavaScript or runtime dependencies. That leaves everything else up to the developer, which is exactly how Virgil likes it.

(Video content)

He appreciates that Jekyll has been around for a long time. It has a mature ecosystem, strong documentation, and an active community. It’s also highly extensible. Since it’s written in [Ruby](https://www.datocms.com/casual-chats/renuo-and-the-birth-of-dato-rails-for-ruby-on-rails-projects.md), writing custom plugins to extend its functionality is straightforward. That flexibility lets him fine-tune each project to match a client’s needs without unnecessary bloat.

> Jekyll might not be shiny anymore, but it’s rock solid. I’d love to see it get more attention, or for someone to build the next great purely static tool.

Not that we're dismissing the other frameworks by any means. He acknowledges that Astro is an impressive product, and frameworks like Hugo and Eleventy offer great performance. But for him, Jekyll remains the best tool because it keeps things as simple as possible.

(Image content)

[*Sound familiar*](https://www.reddit.com/r/ProgrammerHumor/comments/x5sle0/something_i_have_noticed_as_juniors_become/)?

### Let's make it about us again 💅

Jekyll solves the problem of generating static websites, but content is a separate concern.

Jekyll is making life easier for Virgil, but even though his clients don't necessarily need to know about the technical side of things, they do need a way to update their sites and know everything will work 1, 2, 3, or more months down the line when they have changes to make.

That’s where we come in 💁‍♀️

(Video content)

Virgil prefers DatoCMS because it offers him the best of both worlds. On the developer side, we provide a highly structured content modelling approach and clean schema management, making it easy to keep projects well-organized. On the editor side, there's a simple, intuitive interface that clients can use without (much) training, one that can be customized and tweaked and extended with plugins.

His clients don’t care what CMS they use. They care about whether they can update their website easily. DatoCMS provides a clean, minimal UI that lets them focus on their content without distractions. The structured content approach also means that content and presentation are fully separated. Clients can edit text, images, and other media while Virgil remains in full control of how that content appears on the final site, how it builds, etc. etc.

(Video content)

One of the few things clients need to get used to is the fact that fully static sites require a short delay between making changes and seeing them live. Instead of hitting "publish" and getting an instant update, they need to wait for the site to rebuild. This usually takes about a minute in most cases, but it’s different from what they may be used to.

In Virgil’s experience, this has never been a dealbreaker. Once clients understand the process, they accept it as part of the tradeoff for a site that’s more secure, faster, and easier to maintain.

### OK, but does all this scale?

Now, let's finally get into talking about the static approach in 2025.

A common argument against static sites is that they don’t scale. Virgil disagrees.

> People assume static sites don’t scale. That’s a myth. The only real question is: how often do you actually update your content?

The real question isn’t whether static sites scale, it’s how often a site needs to be updated. If a website has thousands of pages but only gets updated once a day, a five-minute build time isn’t a problem. If content needs to change every five minutes, then static might not be the best choice, something with partial hydration or a more dynamic approach might make more sense.

But in most cases he's seen, websites tend to be, essentially, “cool interactive brochures”, and don't need all the dynamic functionality of many modern frameworks.

For Virgil’s clients, frequent content updates aren’t the norm. They update their website a few times a week at most. In that scenario, the benefits of static outweigh the minor inconvenience of waiting for a rebuild.

> Why call the database if you don’t need to? Why introduce that complexity when a static build does the job perfectly?

Static sites also scale exceptionally well for high-traffic scenarios. Since they don’t rely on a traditional backend server, they can be deployed entirely to a CDN. That means pages load almost instantly, even under heavy load. There’s no risk of a database getting overwhelmed or a server struggling to handle concurrent users.

> Static websites are faster, more secure, and scale incredibly well because they run on CDNs instead of constantly hitting a server.

Another advantage is security. With no database or server-side code, there’s very little attack surface. There’s no risk of SQL injection, no server vulnerabilities, and no need for constant security patches. For clients who prioritize reliability and security, static sites offer a level of peace of mind that dynamic sites can’t match.

### Why Static ain't dead

Static sites had "a moment" a few years ago. There was a wave of excitement around static site generators as an alternative to WP and other traditional CMS platforms. Then, the trend shifted towards hybrid approaches. Newer frameworks started blending static rendering with dynamic capabilities.

Virgil sees this evolution as part of the natural cycle of web development, which as we know, goes full circle every couple of years. As complexity increases, there’s often a point where people will start to re-appreciate the simplicity of earlier approaches.

(Video content)

He believes static sites might see another resurgence. As more devs become frustrated with overheads of modern JS-heavy frameworks, they may rediscover the benefits of a truly static-first approach.

For that to happen though, SSGs, both the old and new, like Jekyll, need more visibility. Jekyll is still actively maintained and used by tens of thousands of websites, but it doesn’t get the same hype as newer tools. Virgil hopes to see either Jekyll get more recognition or for a new purely static site generator to emerge that captures the same level of excitement as an Astro or Svelte.

Virgil isn’t against modern web frameworks. He simply believes in using the simplest possible tool for the job. For most websites, that means a static site generator like Jekyll combined with a [headless CMS like DatoCMS](https://www.datocms.com/use-cases/modern-websites.md). Static had a moment, then hybrid frameworks took over. But as complexity grows, there’s always room for a return to simplicity.

For Virgil, static is far from dead. It’s just waiting for more to remember why it worked so well in the first place.

---

# On going beyond the CMS with Plugins

Source [casual-chats]: https://www.datocms.com/casual-chats/on-going-beyond-the-cms-with-plugins.md

## Customer Stories


(Image content)

In conversation with Victor Zumpolle (Frontend Developer)

For most teams, a [Headless CMS](https://www.datocms.com/academy/headless-cms/introduction-to-headless-cms.md#what-is-a-headless-cms) is juuuust a tool. Plug in content, pull it out via an API, and ship it to the frontend. Done.

But for the team at [De Voorhoede](https://www.voorhoede.nl/), DatoCMS goes beyond just a content database. It's a platform they actively extend and shape, because, as they’ve learned, the last 10% of project requirements are always where the real friction lives.

That’s where Victor comes in.

Victor has been with De Voorhoede for over six years, and alongside his colleagues, he’s worked on a long list of DatoCMS powered websites, each with its own quirks and needs. Over time, he realized that many of the missing pieces, those pesky little things that slow down editors or force developers into unnecessary workarounds, could be solved not by changing the CMS itself, but by building on top of it.

> Plugins were an easy way to make the project just that little bit more special for the client, but also for us.

That’s how Victor became the “plugin guy” at De Voorhoede. He’s now authored or contributed to the majority of the [plugins](https://www.datocms.com/marketplace/plugins.md) De Voorhoede has released for DatoCMS. And these aren’t just for fancy showcase projects, they’re practical, purpose-built tools that came directly from real-world problems.

The best part?

[They ain't even private](https://www.datocms.com/partners/voorhoede.md)! They're there for everyone to use.

### Why Plugins?

What Victor and his team love about DatoCMS is how cleanly it separates developer needs from editor needs. On one hand, developers can model content exactly how they want, structured, modular, API-driven, you know, all the things a Headless CMS is supposed to do. On the other, editors get a straightforward, user-friendly UI that doesn’t require technical knowledge.

That clean split is why De Voorhoede uses DatoCMS on many of their projects. But that same flexibility also means that the core product can’t possibly cover every niche requirement (I mean we're pretty open about how [we don't throw in too many bells and whistles](https://www.datocms.com/features.md) to make things overdone). So when specific client needs arise, or when small frustrations repeat across projects, plugins are the natural solution.

(Video content)

For Victor, plugin development started small, literally copying some CSS and button styles from DatoCMS to make an internal tool look native. But the more projects they delivered, the clearer it became that they could package these small fixes as proper plugins. Not just to solve the problem once, but to make it easier for other teams facing the same issues.

And not just for their clients.

They started open-sourcing them, contributing back to the DatoCMS community and giving other developers and editors access to the solutions they’d built.

> A good plugin, for me, is one clear problem, one clear solution.

But we're not here to talk about alllll of them, this is about Victor and his contributions, so let's stick with that, and dive in to his 3 faves, and not all the ones he's help bring to life.

The JSON Table, Computed Fields, and Translate Fields 👇

### The JSON Table

One of the first plugins Victor worked on (and one of the very first in our marketplace) was the JSON Table plugin. Like most of their plugins, it wasn’t born out of a side project or a theoretical use case. It came from a real client need.

The problem was simple but annoying. Several projects involved editors managing key-value data in JSON fields (think translations, settings, or configuration pairs). For devs, a JSON string was fine. For editors, it was a nightmare. The raw JSON view was intimidating, technical, and prone to errors.

> I think the JSON Table was one of the first plugins made for DatoCMS when there weren’t really any plugins yet.

Victor and his team realized that what editors needed wasn’t raw data. They needed a clear, simple table view where they could see and edit key-value pairs without worrying about all the messy JSON syntax.

(Video content)

So they built it.

(Video content)

The JSON Table plugin turns a standard [JSON field](https://www.datocms.com/user-guides/content-modeling/intro-to-fields-in-datocms.md) in DatoCMS into an interactive table. Editors can easily add, edit, and remove key-value pairs without touching any code. It’s visually clean, intuitive, and editor-friendly. No more explaining how JSON works to non-technical users. No more copy-pasting curly braces and worrying about missing commas.

It immediately made their clients' content management smoother. But they also knew this wasn’t a niche problem. So they open-sourced the plugin, making it available to anyone using DatoCMS.

Over time, the plugin evolved. As the DatoCMS plugin system matured, so did the JSON Table. It moved from hacky beginnings, to a proper, well-documented, community-supported tool. Today, it’s one of the more widely used plugins in the ecosystem, live in 750+ projects, precisely because it solves such a common problem so well.

[Check out the JSON Table plugin on the DatoCMS marketplace](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-json-table.md) →

### Computed Fields

The next major plugin Victor worked on was Computed Fields. And like the JSON Table, it came from repeated frustration.

If you're unfamiliar with the approach, Computed fields are inspired by computed values and props that you see a lot in frameworks. Somewhere some values change, and the subsequent computed value changes because of that. This is nifty given how often the use case might arise for a variety of reasons given that developers and editors often needed to generate field values based on other fields.

(Video content)

A classic example from Victor's own experience? Combining a date and title field to create a unique page title. Sometimes it was about generating slugs. Other times it was about creating internal IDs or labels. You get the idea.

The problem was that, traditionally, this kind of logic lived outside the CMS. You’d write a transformation function in your frontend code or in your backend API layer. That worked, but it also meant editors had no visibility or control over how those values were generated. And worse, small content model tweaks often meant dev involvement.

(Video content)

Victor wanted to bring that logic inside DatoCMS, so editors could see and control what was happening without needing a developer.

The Computed Fields plugin does exactly that. It allows editors (or developers setting up the CMS) to define small functions that generate a field value based on other fields in the same record. It’s flexible enough to handle simple concatenation but powerful enough to pull in data from related models, combine images, or even generate dynamic content snippets.

> Computed Fields is made to do anything you like. You can even pull in content from other models or combine fields.

For Victor, the flexibility is the point. Computed Fields is designed to be as broad as possible. It’s a little engine inside DatoCMS that lets you wire up content logic without leaving the CMS.

One of the more advanced use cases the team implemented involved pulling in data from related models, using images and content from one model to generate a field in another. But the real power of Computed Fields is how open-ended it is. Editors and developers are constantly coming up with new ways to use it.

> Talking about it, we keep coming up with new ideas for Computed Fields—that’s why we built it the way we did.

The plugin is a toolkit, not a rulebook.

Victor likes it that way.

And so do we.

And so do you, evidently, given how it's live in over 1,200 projects!

[Check out the Computed Fields plugin on the DatoCMS marketplace](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-computed-fields.md) →

### Translate Fields

And thirdly, of all the plugins Victor worked on, Translate Fields is probably the most complex.

Anyone who’s worked with multilingual content knows how tedious and repetitive translation workflows can be. Copying and pasting content between languages, managing consistency, and avoiding human error, it's a chooooore.

Victor and his team had clients who needed to speed this process up. So they built Translate Fields, a plugin that integrates translation services directly into DatoCMS. Editors can click a button to auto-translate specific fields, cutting down hours of manual work. The sweet bit? Works on modular content and blocks too!

> With the Translate Fields plugin, editors can translate entire fields with one click. It’s easy to use, but hard to build.

The technical challenge wasn’t small. DatoCMS’s content structure is modular and flexible. Fields can be nested, relational, or part of complex models. Figuring out which fields needed to be translated, which ones to skip (like images), and how to handle different content types was tricky. Victor ended up writing a whole system to parse models and apply translations intelligently 🤯

(Video content)

The plugin supports multiple translation services, including DeepL and OpenAI’s APIs. Editors can choose which service to use based on their language needs and budget. The plugin doesn’t claim to deliver perfect translations, there’s still a place for human review, but it removes a huge chunk of the manual effort.

How does it perform in real life? Well, its no surprise that it's also one of THE most popular plugins, live in almost 500 projects at the time of writing this.

[Check out the Translate Fields plugin on the DatoCMS marketplace](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-translate-fields.md) →

### Why OSS and what's next?

Releasing these plugins to the community wasn’t a purely altruistic decision. For Victor and De Voorhoede, it made sense. If they’d already solved a problem, why not share the solution? Why keep it hidden away in just a few client projects?

But OSSing things are and aren't free. It comes with overheads - pull requests, bug reports, questions, feature requests. For a while, Victor was the only person maintaining the plugins, which meant that when project work got busy, plugin maintenance often had to wait.

> Maintaining open source plugins is hard. There were months where I couldn’t work on them and the issues kept piling up.

They’ve since expanded plugin development across the team. Other developers now contribute to plugin maintenance and development, spreading the load and ensuring the plugins stay up to date.

There’s also an ongoing tension between keeping plugins simple and adding every requested feature. Many plugins started life as client-specific solutions, so opening them up for public use raised questions. Should they make the plugins more flexible and broad, at the cost of complexity? Or keep them focused on one clear problem?

(Video content)

For Victor, the answer is somewhere in the middle. The best plugins are opinionated enough to be useful out of the box, but flexible enough to adapt to different workflows.

And that's how they feel about things going ahead too.

Recently, the team released a [phone number plugin](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-phone-number.md) that validates and parses phone numbers, returning metadata like country codes and validation status. They’re also working on a plugin for adding custom styling to structured text fields, so editors can preview content formatting directly inside DatoCMS.

---

# On building Astro themes for multi-brand websites

Source [casual-chats]: https://www.datocms.com/casual-chats/on-building-astro-themes-for-multi-brand-websites.md

## Customer Stories


(Image content)

In conversation with Mojtaba Seyedi (Frontend Developer)

Looking to dive in to the theme and set up your own project?

[Check out MultiLaunch over on the official Astro themes library](https://astro.build/themes/details/multilaunch-multi-brand-website-template/) →

Now on to the good stuff 👇

(Video content)

Managing multiple brand websites or regional variants is one of those problems that hits different. One site becomes five. Five become fifty. Before you know it, you’re juggling different CMS setups, codebases, design systems, and dev teams, just to keep things up and running consistently.

That’s the problem Mojtaba and [Bejamas](https://bejamas.io/) wanted to solve with MultiLaunch.

> If you’ve got 50 brands, you shouldn’t be building 50 different sites from scratch. MultiLaunch is about showing that it doesn’t have to be that chaotic. You’re not maintaining ten stacks. You’re maintaining one. And that changes everything.

[MultiLaunch is a monorepo](https://github.com/bejamas/astro-dato-multilaunch) theme built with Astro, DatoCMS, and Vercel, designed specifically for companies that need to launch and manage multiple websites efficiently. It’s fast, flexible, and comes with the tooling and structure needed to scale across brands or markets, without scaling your tech debt.

(Video content)

Drawing from real-world work with clients (which we'll conveniently choose not to mention because they had the nerveeee to use another Headless CMS for the project that inspired this🫸 ), [Mojtaba](https://www.linkedin.com/in/mojtaba-seyedi/) helped build MultiLaunch to be more than a codebase to approach multi-site architecture using modern tools that favor simplicity, speed, and future-proofing.

So we caught up with him to walk us through everything he's put together.

### Scaling websites with multiple brands

Imagine a company with 5 brands. 10. 25. Ok 50 brands. Now imagine they want a landing page for each brand, each localized to one or multiple regions, each with slightly different messaging or layout tweaks.

That’s not uncommon, and it’s usually handled in the most chaotic way possible.

Different tech stacks. Different CMSs. Different vendors. Different deployment workflows. It works, until it really realllly doesn’t.

(Image content)

While massive enterprises might absorb the inefficiency, smaller companies, or teams trying to grow into new markets, don’t have that luxury. The operational load becomes overwhelming. Launches slow down. Teams get isolated. Costs creep up. And eventually, the inconsistency across websites starts to reflect poorly on the brand.

MultiLaunch was built to offer an alternative. One codebase. One CMS. One pipeline. Multiple outputs. Mojtaba calls it an architectural solution first, and a dev template second. It's a visual and technical proof that this kind of setup doesn’t have to be scary—or expensive.

From a dev's point of view, the value is obvious. You’re not starting from scratch every time. You’re not managing ten repos for ten brands. You don’t have to duplicate your component library or tweak CSS endlessly across isolated sites.

You get a single monorepo that handles every brand or region as a structured piece of a bigger system. That monorepo houses separate apps, one for the parent brand and others for sub-sites, alongside a shared UI package. Everything lives together. Everything is reusable. Nothing is duplicated unless it has to be.

But Mojtaba didn’t build this just for devs.

MultiLaunch is also made for content teams. Editors don’t need to think about deployment or Git. They don’t need to manage multiple logins across different CMS platforms. They work in one DatoCMS instance, with [roles and permissions](https://www.datocms.com/docs/general-concepts/roles-and-permission-system.md) controlling what they can see and edit.

And because the content modeling is clean and consistent, editors can publish localized content quickly without having to understand how the underlying site is built.

The result? Faster time to market. Better brand consistency. Less internal chaos.

### But why Astro, Dato, and Vercel?

MultiLaunch wouldn’t work without the right stack. And each tool in the stack plays a specific role.

[Astro](https://astro.build/) forms the frontend foundation. Mojtaba chose it because it’s fast, but more importantly, because it doesn’t overcomplicate things. Astro starts simple, just HTML and templates, but it scales with your needs. If you want interactivity, you get it on demand. If you want performance, it’s already there by default.

> Astro is simple when you need it to be, but grows with you when your needs get complex. That’s why we picked it.

Because Astro ships zero JavaScript unless you ask for it, sites are fast. Page loads are instant. SEO scores are excellent. And for marketing pages and regional landing sites, that matters.

(Video content)

*Psst: Our own DatoCMS website recently moved to Astro, so if you're looking for a deep dive into why we did that,* [*check out our post*](https://www.datocms.com/blog/why-we-switched-to-astro.md) *on the topic.*

Anyways*.*

DatoCMS sits at the core of the content layer for this approach. From Mojtaba’s point of view, it strikes the perfect balance between flexibility for developers and simplicity for editors. He’s used other CMSs. He’s seen the complexity.

(Video content)

With DatoCMS, he gets [structured content, localization, roles, permissions, and previews, out of the box](https://www.datocms.com/features.md). The GraphQL API is straightforward. The editor interface is clean. And the plugin ecosystem makes it possible to extend the UI in smart ways.

And then there’s Vercel. Deployment is seamless. Scaling is handled automatically. Preview branches spin up instantly. From zero traffic to millions, the infrastructure doesn’t need babysitting. Mojtaba didn’t have to over-optimize anything, Vercel just works.

Together, this trio gave Bejamas exactly what they needed to build MultiLaunch. Each tool solves a part of the problem without locking you in. That flexibility was key.

### OK, now let's make it about us again 💁‍♀️

The foundation of MultiLaunch is its content model. Everything is structured to treat the company as one system, with individual brands or regions managed as modular pieces.

(Video content)

*Not an ad, Mojtaba. Not an ad* 😘

Anyways. Moving on.

At the top level, there’s a `home` model representing the parent brand. Then, there’s a `brand` model for each sub-brand or market site. Each brand model includes fields for products, reviews, forms, and any other content unique to that brand.

There’s also a layout and theme model that lets editors define how each site should look and behave, whether it’s colors, structure, or layout options.

Mojtaba deliberately kept the schema really simple and straightforward. The point was to make the architecture clear and understandable for teams evaluating how to scale content operations. He wanted teams to look at the schema and immediately see how the pieces connect.

It’s not abstract. It’s practical.

> I’ve worked with a lot of CMSs, and DatoCMS is the one I’d hand to a non-technical editor without hesitation.

And while the content model provides structure, the plugin ecosystem is what makes the editorial experience actually enjoyable.

Mojtaba used four main plugins in MultiLaunch, each with a clear purpose.

[Visual Select](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-visual-select.md) was used to give editors layout choices without digging through dropdowns. If a contact form had a horizontal and vertical variation, editors could preview both and pick the one they wanted visually. No guesswork.

> The Visual Select plugin is one of my favorites. It gives editors real layout control without overwhelming them.

[Star Rating Editor](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-star-rating-editor.md) came in for the reviews component. Instead of typing in numbers, editors just clicked the appropriate number of stars. Small touch, but a big improvement in usability.

[Web Preview](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-web-previews.md) was a must. Editors need to see how content looks before publishing, and DatoCMS’s live preview integration made that seamless with frontend visibility right in the CMS.

Then there’s the [AI Translator](https://www.datocms.com/marketplace/plugins/i/datocms-plugin-ai-translations.md) plugin. This one, according to Mojtaba, saved him a lot of pain. During development, as the content model evolved, he had to reset and restructure content multiple times. Since the template supported five languages, that meant hours of re-entering data.

> The AI Translator plugin saved me a lot of tears. When you restructure content models mid-project, it’s a lifesaver.

With the translator plugin, it became a one-click process. Users could populate translated versions instantly using OpenAI or DeepL. And while the translations aren’t always perfect, they’re more than enough to start. Teams can tweak them later if needed.

The combination of structured modeling and plugin enhancements made the template feel polished, not just functional.

### Peeking under the hood

Behind the scenes, MultiLaunch is built as a monorepo.

Everything lives in one codebase. That includes the Core app for the main brand, the Brands app for regional sites, and a shared UI package with all the components.

The UI package is one of the most important pieces. It keeps design consistent across every site. It also lays the groundwork for a future design system. Instead of rebuilding nav bars, hero sections, or forms for each site, everything is shared—and can be styled or extended per brand if needed.

Mojtaba didn’t try to over-engineer the structure. The goal was to make it easy to understand, easy to duplicate, and easy to extend. Whether you’re launching two sites or twenty, you don’t need to rethink how things are built.

Whipping up a new brand? Simply add the brand slug into Vercel as a new variable, and you've got everything set up.

And speaking of Vercel...

Deployment is handled entirely through there. You push your code, and the site goes live. You publish content, and DatoCMS triggers a build.

Astro’s static site generation keeps build times fast and output lightweight. Because it’s static, there’s very little to optimize, it’s fast by default.

> With Astro and Vercel, performance just happens. I didn’t have to fight to make the site fast—it was fast from day one.

Still, Mojtaba put in some work on the details.

To prevent layout shifts, he pulled image width and height into the frontend. That improved Core Web Vitals and made pages feel smoother. He also added a smart image-switching component that lets editors upload light and dark mode versions of the same asset, switching them based on the site’s theme.

Again, small touches, big result.

From dev to deploy, the entire workflow is meant to feel clean. It’s not trying to be fancy—it’s trying to be usable. And it is.

---

At its core, MultiLaunch is about solving problems that nearly every mid-sized digital team faces. Launching new sites takes too long. Editors don’t have a consistent experience. Teams get fragmented. And the complexity gets out of hand.

MultiLaunch answers that with a simple proposition: keep the architecture tight, the tools flexible, and the workflows smooth.

Everything from content modeling, to plugin selection, to frontend stack was picked to support that idea. You’re not fighting the CMS. You’re not duplicating work. You’re not maintaining five different stacks.

You’re shipping content faster. You’re managing growth. You’re staying consistent across regions and brands. And you're doing it without adding unnecessary overhead.

That’s the real power of this project. It’s not flashy for the sake of it. It’s practical, reusable, and battle-tested.

So, if you’re running more than one website, or plan to, it’s probably worth [checking out MultiLaunch and giving it a go](https://astro.build/themes/details/multilaunch-multi-brand-website-template/).

---

# On building visually stunning projects at scale

Source [casual-chats]: https://www.datocms.com/casual-chats/on-building-visually-stunning-projects-at-scale.md

## Customer Stories


(Image content)

In conversation with Vinicius Silva (CTO)

[Webcore](https://www.datocms.com/partners/webcore.md) builds digital experiences that moooove.

Scroll-triggered transitions, immersive media, parallax, motion layers, and custom animations are all standard fare for them. Their work feels more like interactive art direction than traditional web design.

But it still needs to ship fast, perform well, and be easy for clients to update without breaking everything.

So what gives?

To make that work, they rely on a battle-tested stack. [Nuxt](https://www.datocms.com/cms/nuxtjs-cms.md) on the frontend. DatoCMS as the content layer. Everything statically generated, cleanly separated, and structured with care. That’s what allows their devs to build without friction and gives content editors the confidence to publish without worrying about how things might look or break.

To get into the weeds of their philosophy and approach, we caught up with Vinicius, CTO at Webcore, who's shared tons of nuggets on how their team approaches projects for their clients.

### A structured foundation for complex design

[Content modeling in DatoCMS](https://www.datocms.com/docs/content-modelling.md) happens before any code is written. The team works from design concepts but starts building in the CMS as early as possible. Every animation, layout section, or feature block needs a clean schema to deliver from.

There’s no generic page model. Instead, they define atomic components: hero sections, carousels, product grids, video embeds, testimonials. Each has its own record type. [Editors can reorder blocks](https://www.datocms.com/docs/content-modelling/blocks.md) and change the content, but layout logic is never exposed to them. That separation keeps designs consistent and makes large builds easier to manage.

> The frontend handles motion and layout and the CMS handles data. This gives users the freedom and flexibility to model and add content as they like, without worrying about the layout or presentation being unintentionally affected

The result is a content system that scales without becoming fragile. Editors can stack blocks, swap media, or write translations without ever breaking a page.

### Visual intensity without performance pain

A lot of Webcore’s work is visually dense. High-res images. Background videos. Multiple animations per scroll. That’s where most projects end up trading visual quality for performance, but Webcore plans ahead.

> Having Imgix and Mux integrated directly in DatoCMS is a game changer. We don't worry about optimizing assets, it's just built in.

Assets are managed through [Imgix](https://www.datocms.com/docs/asset-api/images.md) and [Mux](https://www.datocms.com/docs/asset-api/videos.md), both of which are fully integrated with DatoCMS. Editors upload what they need, and the system handles optimization, streaming, and delivery. Video is never handled manually, and image formats adapt automatically.

(Video content)

On the frontend, everything is generated statically through Nuxt. No runtime bottlenecks, no unpredictable fetches. Performance is designed in, not patched on later.

This setup also allows for smooth builds even when dealing with large projects. Using [DatoCMS’s batch API](https://www.datocms.com/docs/content-delivery-api/how-to-fetch-records.md), they reduce the number of API calls needed to pull content. This keeps build times short and avoids the usual headaches that come with visual-heavy sites.

### Sticking with Nuxt over React

While nothing in frontend is ever stagnating, and with all of us constantly jumping from one new framework to the next, Webcore has stuck with Nuxt. Vue fits how they work, and Nuxt has given them a level of stability that they don’t get with most other frameworks.

(Video content)

Routing and configuration come out of the box. There’s no need to stitch together a dozen plugins to get things running. [Nuxt plays well with static builds, integrates cleanly with DatoCMS](https://www.datocms.com/docs/nuxt.md), and lets them deliver fast sites without losing flexibility.

According to Vinicius, one of the biggest advantages is that they’ve been able to build a set of internal patterns they can reuse without worrying about version churn or breaking changes every few months.

For content-heavy builds where performance, design precision, and editor safety all matter, that kind of stability adds up fast.

### Editor experience that doesn’t get in the way

Webcore’s content editors aren’t expected to think like designers. They’re not dragging layout components or tweaking margins. They’re selecting from structured content models and focusing on messaging, assets, and data.

> We’ve worked with other CMSs before. DatoCMS is by far the fastest for us to model and ship real content with structure.

DatoCMS gives them a clean interface. Field names are clear. [Localization is built in](https://www.datocms.com/docs/content-delivery-api/localization.md). Previewing is straightforward.

(Video content)

One feature the Webcore team leans on a lot is [environments](https://www.datocms.com/docs/general-concepts/primary-and-sandbox-environments.md). When trying out a new block, testing a design variation, or giving a client a place to experiment, they duplicate the content environment. No production risk, no migration scripts, just a clean fork of the schema and content. That saves time and gives teams room to move fast without fear of breaking things.

> We treat DatoCMS almost like a database. It’s not just for editors. It’s a structured content API for us. We’ve used environment cloning more times than I can count. You want to prototype something? Duplicate, test, and delete. No stress.

They also build [preview environments](https://www.datocms.com/docs/general-concepts/primary-and-sandbox-environments.md) that match the frontend 1:1. Editors see exactly what their content will look like before it goes live, down to animations and styling. That kind of confidence reduces back-and-forth and gives clients the trust to publish on their own.

### Designing for performance from the start

What sets Webcore apart isn’t just the visual style. It’s how they treat performance as a design constraint, not a tradeoff. The stack supports that, but the mindset is what really makes it work.

They avoid bloated schemas. They don’t build catch-all content models. Every asset is optimized before it hits the browser. And every visual effect is scoped with intention. Nothing gets added unless it’s been thought through.

DatoCMS lets them keep structure clean and predictable. Nuxt keeps the frontend fast and scalable. And the glue between them is a clear set of roles. Developers handle structure and rendering, editors handle data, and designers stay focused on visuals.

It’s not flashy for the sake of being flashy. It’s a system built to deliver big visual impact without the mess.

---

Webcore’s work looks great, moves well, and doesn’t break under pressure. That’s not just design. It’s structure. It’s the result of building systems that let developers move fast and editors stay confident.

> For us, the stack isn’t about hype. It’s about stability, speed, and letting each part do its job. DatoCMS handles that well. The performance side of DatoCMS is super solid. Especially with batch fetching and static builds, we’ve never had an issue.

DatoCMS plays a big part in that. The performance wins, the modeling flexibility, and the simplicity of the editor UI are what let them take on complex builds without complexity bleeding into the workflow.

The stack is stable. The experience is clean. And the results speak for themselves.

---

# On helping shape the GraphQL Community

Source [casual-chats]: https://www.datocms.com/casual-chats/on-helping-shape-the-graphql-community-as-we-know-it.md

## Customer Stories


(Image content)

In conversation with Julian Bauer (Co-founder and Managing Director at Overnice)

If you've written a GraphQL query in the last few years, in or out of DatoCMS, there's a solid chance you've used a tool that [Julian Bauer and the fine folks over at Overnice](https://www.datocms.com/partners/overnice.md) helped design.

(Video content)

Using the GraphQL Explorer (Stolen with permission from Overnice)

You might not know it, but the [GraphQL Explorer you use in DatoCMS](https://www.datocms.com/docs/content-delivery-api/how-to-fetch-records.md), or any other GraphQL CMS, or countless other GraphQL-powered tools, has roots in the work he did with his team years ago.

Today, he's still heading up Overnice, one of our friends in the DatoCMS partner ecosystem, building, in his words, "unnecessarily nice brands and products".

[Overnice](https://overnice.com/) is a Berlin-based brand and product studio with a knack for turning complex, sometimes dry products into something that feels intuitive and joyful. “We like the challenge of taking something complicated, maybe even boring, and making it approachable and accessible,” Julian says. “Why should good UX be reserved for lifestyle brands?”

Touché.

Anyways, we're not here to talk about their projects today. Heck, we're not even here to talk about their work with us. We're looking to dive into the GraphQL explorer and revive some old memories from the early days ☕️

### The birth of the GraphQL Playground

The story for Overnice and Julian started back in 2017, when [Prisma](https://prisma.io/) was still called Graphcool. “They were building a backend as a service based on GraphQL. The .cool TLD had just come out, so Graphcool made sense,” Julian laughs. “They needed a GraphQL Explorer. GraphiQL existed, but it was missing features and, honestly, it was ugly.”

If you're new to the space or don't remember the OG GraphiQL, here's what it looked like 👇

(Image content)

A Front End Developer’s Guide to GraphQL (CSS Tricks, Peggy Rayzis on Dec 4, 2017)

Working with Prisma, they helped design the GraphQL Playground, a more polished, user-friendly alternative to GraphiQL.

(Image content)

Foundations of the new Explorer (Stolen with permission from Overnice)

It was open sourced and quickly gained fans. “GraphiQL was still the standard, but Playground attracted devs who cared about the DX,” Julian remembers.

Side note: They also collaborated on [How to GraphQL](https://www.howtographql.com/), the interactive tutorial on GraphQL from Prisma that’s still popular today!

Since then, Prisma moved away from GraphQL to become the TypeScript ORM to now a serverless Postgres DB, but all the work they put into the GraphQL space has helped shape what it is today.

### Today's GraphQL Explorer

Fast forward to 2022. The GraphQL community wanted a single standard tool. The idea was to merge the best of GraphiQL and Playground. Overnice, as the team behind the original Playground, was brought back to handle the redesign along with the fine folks at [Stellate](https://stellate.co/) (then GraphCDN), who had worked with Julian back when they were at Prisma.

So many early GraphQL days memories brewing...

Anyways.

The "redesign" of the Playground was focused on making a genuinely gorgeous tool that focused on the DX and provided a really smooth and intuitive experience. Docs, actions, env switchers, everything should be in the right place where they belong.

(Image content)

Everything in the right place (Stolen with permission from Overnice)

Julian remembers a small but important change: the play button.

> In the original GraphiQL, the play button was way up in the top left. You typed in the editor, moved your eyes left, then back right to see the result. It was this weird zig-zag flow. We put the play button in the middle, between the panes. It felt odd at first but now it’s natural.

(Video content)

Another focus was making the docs explorer feel more connected to the editor.

> Before, it was like two separate products in one. You’d look up your schema in the docs, then manually type it into the editor. We wanted you to click fields directly in the docs to build your query. So we moved the docs to the left, made them the starting point, and added a tree structure to click together a query.

(Video content)

This “click-to-query” model was inspired partly by other tools but made its way into the modern Explorer, and it’s now the default for many dev-focused products.

### From Designers to Users

Of course, if you've been working in the space (especially building websites) long enough, you'll see yourself go from tool creator to tool users, and things were no different for Julian and the Explorer, especially given how deeply ingrained into Headless CMS tools it's become.

> It literally happened when I opened DatoCMS one day and thought, wait a minute… isn’t that the GraphiQL we designed? It made sense that you guys would use it, but it was still a cool moment.

For Overnice, the Explorer isn’t just a past project. It’s a core part of their workflow today. Even in recent projects where they were tasked with building a multi-domain, multilingual site with a complex role and permissions setup, the Explorer was crucial in testing out all their queries before moving it to the repo.

“Before we wrote a single line of front-end code, I used the Explorer to prototype the schema. Could an admin see everything? Could a local maintainer see only their events? Would the multilingual structure query cleanly? It let us test all of that before building anything.”

That schema-first approach saved them hours, if not days, of development time.

A real I-should-pat-myself-on-the-back-for-this moment.

### Dev workflow candy

One thing Julian is particularly proud of is how adaptable the Explorer turned out to be.

“It can be ultra complex with tabs, variables, docs, and multiple panes. Or it can be stripped down to just an editor and results window inside a docs page. But it doesn’t feel like a generic boilerplate in either case.”

That adaptability matters when a single tool ends up in tons of products, each with its own brand and UX quirks.

> We wanted it to feel like it belonged wherever it was embedded, but still feel designed. That’s a fine line.

(Video content)

And it's true. In DatoCMS it really looks like a "core native" part of the product, as is the case with many other CMS tools that embed the explorer.

Julian's more of the design guy, so he really wanted a more up-to-date account of how devs feel about the Explorer in the context of today's GraphQL landscape. So we had a chat with his teammate Jonny, who works most with GraphQL day to day, to ask how it fits into his workflow.

The magic combo for him? [Fragments](https://www.datocms.com/docs/content-delivery-api/modular-content-fields.md) + type generation with [`gql.tada`](https://gql-tada.0no.co/) paired with the Explorer for testing things out👇

(Video content)

“He keeps the part of the query that’s relevant to a component right next to the component itself. Update the block, update the query. With `gql.tada`, you get types from that instantly, with autocomplete and type safety. And if something’s off, you debug in the Explorer. It just feels like the way it should be.”

That tight loop? Write your query, see your types, test in the Explorer? That's become a core part of so many dev process when working with GraphQL, it's honestly crazy to think about just how widespread this has become.

When asked if he expected the Explorer to become as widely adopted as it is, Julian pauses. “Not really. Back when we did Playground, GraphiQL was still the big one. I think it only sank in when we were redesigning GraphiQL itself. Suddenly I saw the GitHub stars and realised… this is everywhere.”

For devs, the Explorer is just another tool in the stack. But for Julian, it’s a reminder that small UX decisions, like moving a button a few centimetres for example, can change the expected DX across an entire ecosystem.

---

# On powering 1K+ pages with ease

Source [casual-chats]: https://www.datocms.com/casual-chats/safetychain.md

## Customer Stories


(Image content)

In conversation with Derrick Threatt (Dir. of Marketing Ops)

## About SafetyChain Software

[SafetyChain Software](https://safetychain.com/) provides a platform to help food manufacturing companies ensure their production processes are efficient and safe.

Led by Derrick Threatt, Director of Marketing Operations, SafetyChain undertook a project to transition their website from WordPress to DatoCMS. We caught up to discuss the challenges faced, implementation, and the results achieved throughout this project.

SafetyChain’s primary objectives were to develop a new marketing website that was scalable, secure, and easy to manage, and the project encapsulated:

1.  Improving website performance and speed,
    
2.  Enhance the website’s overall security,
    
3.  Simplify content management for the editorial team, and
    
4.  Ensure flexibility for future growth and integrations, potentially migrating to Next.js
    

(Video content)

## TLDR

-   **Performance and Security**: Transitioning from WordPress to DatoCMS enhanced website speed and security, addressing major pain points around plugins, content structure, and general scalability.
-   **Content Process**: Modular Content, Blocks, and Dato’s intuitive interface simplified content creation and editing, reducing the need for training and ensuring consistency. Modular content allowed enough “guardrails” to be established for content to independently create new pages and posts with ease.
    
-   **Customizable and Flexible Integration**: The new stack, including Astro, Tailwind, and GraphQL, integrated seamlessly with DatoCMS, allowing for flexible and dynamic content management, with the CDA playground providing a great testing ground for complex queries.
-   **Support and Reliability**: Minimal downtime and prompt support from DatoCMS ensured a smooth project completion.
    
-   **Future-Ready Infrastructure**: Plans for localization and exploring migrating to Next.js for live previews and server-side rendering highlight SafetyChain’s plans for growing their website and introducing new enhancements for their content team.
    

## Challenges

SafetyChain’s original website was on WordPress, and several challenges led them to [exploring a move towards Headless](https://www.datocms.com/academy/headless-cms/headless-cms-selection-criteria.md):

1.  **Performance Issues**: The existing WordPress site experienced significant slowdowns, affecting user experience and SEO performance.
    
2.  **Security Concerns**: Frequent security vulnerabilities in WordPress posed risks that were a concern for SafetyChain, particularly when considering their plugin ecosystem.
    
3.  **Complexity and Flexibility**: WordPress's UI felt overly complex for the editorial team with their implementation, making new additions and customizations difficult and time-consuming from an engineering standpoint.
    
4.  **Plugin Problems**: The WordPress ecosystem’s reliance on numerous plugins led to compatibility issues and maintenance headaches for the team.
    

## Implementation and Solution

After evaluating several options, including WordPress, Headless WordPress, and other Headless CMS, SafetyChain selected DatoCMS for a variety of reasons:

-   **Speed**: [DatoCMS significantly improved website performance](https://www.datocms.com/features/worldwide-cdn.md), addressing the critical issue of slow load times experienced with WordPress.
-   **Security**: Aside from API security and tokens for access, the team noticed a lack of security “dependencies” since there weren’t any 3rd party dependencies when it came to plugins and functionality.
    
-   **Flexibility and Simplicity**: DatoCMS allowed for a more user-friendly setup, where only necessary elements were exposed to the content editors, thereby simplifying their workflow. In fact, one point that stood out was the lack of “handholding” needed for the content team – once changes were implemented, the platform was intuitive enough that the team just “got it”.
    

(Video content)

-   **Customizable Plugins**: The ability to develop [custom plugins](https://www.datocms.com/marketplace/plugins.md) ensured that SafetyChain could tailor the CMS to their specific needs without relying on potentially unreliable third-party plugins. Between the library of community plugins and the ability for SafetyChain to build their own private ones (for example, a plugin for videos from Wistia), they preferred the approach to customizing the CMS in comparison to WordPress.
    

(Video content)

The implementation process was extremely smooth, given it was well planned and broken down into concrete milestones.

Once their evaluation of multiple CMS platforms was complete, they proceeded to build out their website with Astro, Tailwind, and GraphQL at the core of their stack. Gradually, they began to introduce their key integrations like HubSpot and Wistia.

The transition to GraphQL from Markdown and JS files was facilitated by DatoCMS’s API playground, which simplified the process of building queries – a particularly used feature throughout the implementation to test out and validate complex queries.

(Video content)

They adopted an iterative development approach using environments to ensure every stage and deployment went smoothly, using staging and production environments to test and roll out changes without disrupting the live site. This approach minimised downtime and allowed for efficient troubleshooting and updates.

## Results and Looking Ahead

Once everything was live in production and the entire sitemap migrated, one of the first things they noticed was that the new setup reduced their build times significantly. With a project comprising over 1,000 pages, deployments only took about 80sec to complete with Cloudflare added into the mix.

The [editorial team, on the other hand, found DatoCMS extremely intuitive and easy to use](https://www.datocms.com/user-guides.md), which drastically reduced the need for training. Editors could now create and modify pages with ease, maintaining brand consistency and ensuring a high-quality user experience.

Modular Content and Blocks allowed editors to build pages from scratch, offering flexibility without sacrificing simplicity – giving them a very WYSIWYG feel. This allowed them to make changes quickly and efficiently, without the risk of breaking the site’s design or functionality, since the content models had enough flexibility with “guardrails” attached.

(Video content)

Derrick also attributes the overall success of the project to some key features that are heavily used, giving the dev and editorial team the best possible experience with Dato:

-   **Modular Content Blocks**: These enabled flexible page creation and editing, allowing editors to build and customize pages dynamically.
-   **CDA Playground**: Facilitated the building and testing of GraphQL queries, making the development process more efficient.
    
-   **Custom Plugins**: SafetyChain developed custom plugins, such as one for Wistia, and are working on one for HubSpot.
-   **Environment Management**: Utilized staging and development environments to ensure safe deployment of changes, minimizing the risk of errors on the live site.
    

Their plans for the future include increasing their Dato adoption by introducing localization – particularly in Spanish and French to better serve their Mexican and Canadian markets. They’re also considering a switch to Next.js to leverage server-side rendering and live-preview capabilities to let editors make faster changes.

---

# On uncovering unconventional use-cases

Source [casual-chats]: https://www.datocms.com/casual-chats/trip-to-japan.md

## Customer Stories


(Image content)

In conversation with Jökull Solberg (CTO and Co-Founder)

[Trip To Japan](https://triptojapan.com/) launched as a tour reseller for tourist experiences in Japan, aggregating hundreds of different operators. Their plan was to grow into a more complex product – a personal itinerary builder. This focuses on letting individuals create a highly personalised, well, trip to Japan (ha!), including all accommodations, in-country travel, and experiences.

## About the project

They primarily used DatoCMS to manage the content of their website, particularly their editorial and SEO content.

However, particularly through features such as links and [localization](https://www.datocms.com/features/headless-cms-multi-language.md), they began to unlock non-conventional use-cases like custom itinerary building and trip curations using DatoCMS.

(Video content)

> *That was not the plan in the beginning. Dato has really kind of nudged its way into unexpected parts of our stack and product, I would say, where you wouldn't necessarily expect a CMS to power things.*

Providing users with the ability to build custom trips via a “shopping cart” like experience meant that the team had to work with highly complex data structures and types, so the Trip To Japan team were very deliberate in the stack and CMS choices they made from the very early days.

## Technical Details

Given that they were going to be focusing on a great user experience for the traveller and wanted an equally good developer experience internally, they opted for a stack that combined [Turso](https://turso.tech/), [GraphQL](https://www.datocms.com/features/headless-cms-graphql.md), Typescript, [NextJS](https://www.datocms.com/docs/next-js.md), and Vercel.

(Video content)

This naturally meant that they were considering a Headless CMS from the very beginning, and after evaluating a few options, Jökull, Trip To Japan’s co-founder, opted for DatoCMS for a few reasons.

-   The ability to (very) easily construct a [GraphQL API](https://www.datocms.com/features/headless-cms-graphql.md) allowing their server to consume content from DatoCMS, eventually using RSC and tRPC between their client and server.
-   A strong editorial experience for their editors to create itineraries as well as blog posts, as they rely heavily on [SEO](https://www.datocms.com/docs/content-modelling/seo-fields.md) and content for their organic approach to growing.
    
-   The functionality of DatoCMS's SEO fields, which is heavily used to help all content be well optimized before being published.
-   In-built [localizations](https://www.datocms.com/docs/general-concepts/localization.md) and locale-based publishing given their focus to have the website available in English, Japanese, Chinese, Korean, and Thai.
    
-   DatoCMS’s [plugin ecosystem](https://www.datocms.com/marketplace/plugins.md) allowing them to create and maintain a plugin to manage machine translations programmatically for each new piece of content, and
-   An insanely simple user-onboarding experience for their content team.
    

Jökull’s future plans with Trip To Japan include launching the granular itinerary builder option for customers, powered heavily by DatoCMS’s \`link\` field, allowing users to “drag and drop” their ideal experiences seamlessly. They’re also working on exciting user-generated content use-cases, by allowing external travellers and influencers to create, modify, and publish their own curated itineraries for other users to gather inspiration from.

Applications like Trip To Japan are constantly pushing the boundaries of what people "expect" a Headless CMS to do, especially when they think of one as a hosted API rather than a CMS, and we're keen to keep an eye on how these use-cases get more sophisticated.

Check out their website on [TripToJapan.com](https://www.triptojapan.com/)!