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

> [!PROTIP] Pro tip: Customize your CMS domain
> 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**.
> 
> To do so, first ensure that the CNAME record of your chosen domain points to admin.datocms.com. Then, go to your dashboard, click on your project, and select “Add custom domain” in the Custom domain section.

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

### Finer grain control on linked records

In case of linked records you can decide which behaviour to have when a record gets published. For example, you can determine if all the linked records should be published as well or if you want to emit a validation error. The same goes for unpublishing and deleting.

In the field validation you can pick the option that you prefer:

(Video content)

To know 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, 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.

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](https://www.datocms.com/blog/how-to-build-a-multi-language-website-with-next-js-i18n.md) 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)

## 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 — 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. 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.

---

# 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 — 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 sidebar.

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

---

# Overview — 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.

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

---

# Overview — 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 — Your first request

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

In REST, HTTP verbs determine the operation performed. In GraphQL, you'll provide a JSON-encoded body even if you're performing a query operation, so the HTTP verb is always `POST`.

##### Curl example

Terminal window

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

##### Vanilla JS

```javascript
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:
    }),
  }
)
.then(res => res.json())
.then((res) => {
  console.log(res.data)
})
.catch((error) => {
  console.log(error);
});
```

##### @datocms/cda-client

We also offer a lightweight, TypeScript-ready package that offers various helpers around the native Fetch API to perform GraphQL requests towards DatoCMS Content Delivery API:

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

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

console.log(result);
```

You can learn more about this package on it's [README](https://github.com/datocms/cda-client) file.

> [!PROTIP] Pro tip: Top 5 JavaScript GraphQL Client Libraries
> 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.

---

# 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 [API Explorer](https://cda-explorer.datocms.com/).

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 — API headers (environments, drafts, strict mode, cache tags, content link)

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

DatoCMS offers a single GraphQL endpoint:

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

The endpoint remains 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.

### Specifying an environment

To explicitly read data from a specific environment you can add the following header:

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

If no `X-Environment` header is provided, the [primary environment](/docs/general-concepts/primary-and-sandbox-environments.md) will be used.

### 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 add a header to access records at their latest version available, instead of the currently published one. This can be useful on staging environments, or your local development machine:

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

> [!NOTE] Good to know: X-Include-Drafts cannot be false
> Please note that this header **must** be set to `**true**` if it's specified at all. If you don't want to see drafts, **exclude the header entirely.** If you attempt to set `X-Include-Drafts:``**false**`, you'll get an error:
> 
> ```json
> {
>     "code": "INVALID_X_INCLUDE_DRAFTS_HEADER",
>     "details": {
>       "message": "X-Include-Drafts header can only be set to `true`"
>     }
> }
> ```
> 
> If you need to conditionally set this header, you can use a pattern like:
> 
> ```javascript
> const headers = {
>   'Authorization': 'Bearer YOUR_API_TOKEN',
>   'Content-Type': 'application/json',
>   ...(isStagingEnvironment && {'X-Include-Drafts': true})
> };
> ```
> 
> This will add the header and set it to true if `isStagingEnvironment == true`. Otherwise, the header will be omitted altogether.

### Strict-mode for non-nullable GraphQL types

If you want to make sure that no invalid record is ever returned without the need to [always specify an `_isValid` filter](/docs/content-delivery-api/filtering-records.md#_is_valid) in your queries, you can add the following header:

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

In contrast to the `_isValid` filter, this header will also have 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 properties 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).

> [!WARNING] This mode requires migration scripts to be safe!
> When you add/remove validations to a model that already has a set of records created, DatoCMS is forced to launch a new set of checks to verify that these existing records continue to be valid or not.
> 
> If you make a GraphQL request with the `X-Exclude-Invalid` header during this re-validation phase — which depending on the amount of records may take several minutes to complete — **the response will be an error**, because `X-Exclude-Invalid` can only return records that are certain to have been properly checked using the latest validation rules available!
> 
> For this reason, we strongly suggest using this header in conjunction with [migration scripts](/docs/scripting-migrations/introduction.md), so that you can make changes to your validation rules inside a sandbox environment, wait for the validation checks to finish, and only after that promote the sandbox environment as primary. This will avoid potential issues and errors for your final visitors.

### Cache tags

To receive the [Cache Tags](/docs/content-delivery-api/cache-tags.md) associated with your query, you need to add the following header:

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

### Content Link

If you have [Content Link](/docs/content-link/how-to-use-content-link.md) available on your project, you must add two headers to signal to DatoCMS Content Delivery API to embed metadata that enable Content Link on websites hosted on Vercel.

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

`X-Base-Editing-Url` can also be used in isolation: it enables the usage of the `_editingUrl` field in the GraphQL API.

---

# 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:

```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:

How to create a GraphQL API Token (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 — 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 [Your first request](/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 — Technical Limits (CDA)

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

> [!NOTE] These are limits for the GraphQL Content Delivery API
> For limits applicable to the REST Content Management API, please instead see [Technical Limits (CMA)](/docs/content-management-api/technical-limits.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.

Here are the technical limits currently in place for the CDA:

-   **GraphQL complexity cost**: 10,000,000 ([read more](/docs/content-delivery-api/complexity.md))
-   **Real-time Updates API**: max. 500 concurrent connections per project ([read more](/docs/real-time-updates-api/rate-limiting.md))
    

#### CDA Rate Limits

The Content Delivery API Rate limits specify the number of requests a client in a specific time frame.

Since this API is meant to be used for delivering content from DatoCMS to apps, websites and other digital products consumed by end-users, **there are no rate-limits enforced on requests that hit our CDN cache**. The calls will still count against the monthly quota, even if cached.

For requests that do hit the Content Delivery API we enforce a rate limit of 40 requests per second and 1,000 requests per minute per API token. When these limits are exceeded, the API will respond with a **status code of 429**.

> [!WARNING] 429 Status Responses in Shared Infrastructure
> Even if you're operating within your rate limits, there's still a chance of encountering a 429 status code while using the DatoCMS shared infrastructure or medium-density infrastructure during peak system load.
> 
> Although this is uncommon, it's wise to be proactive in dealing with this situation. To handle it effectively, it's recommended to implement a retry mechanism. If you receive a 429 status response, wait for a few seconds before attempting the request again.

Cache gets invalidated selectively based on the objects referenced in the query and the payload. As a rule of thumb, when you update a specific model/record/asset, all GraphQL queries that reference those object will get invalidated. Each PoP will then make new requests to the Content Delivery API, and cache the new result for future requests.

#### HTTP Response Headers for CDA Limits

Each CDA response contains several HTTP headers that helps you stay within the limits:

-   `x-cacheable-on-cdn`: Indicates whether the request can be cached by our CDN (either `true` or `false`)
-   `x-cacheable-on-cdn-query-length-limit`: The actual size of the compressed body request in bytes (e.g. `273/8192`)
    
-   `x-complexity`: The estimated [GraphQL query complexity](/docs/content-delivery-api/complexity.md).
-   `x-request-id`: A DatoCMS internal-use UUID that helps us troubleshoot specific API requests. If you're encountering problems with the CDA, whether related to limits or otherwise, logging this UUID will help our support team better diagnose the problem.
    
-   `cf-cache-status`: Either `HIT` or `MISS`. Cached requests are NOT subject to rate limits, but are still billed per-request (i.e., a response of `HIT` is not rate-limited, but still costs you 1 CDA request credit).
    
    Following an invalidation, the first CDA request to hit a particular CDN edge node will always be `MISS`, since the CDN has to fetch refetch the query from our origin. Subsequent repeats of the same requests to the same edge node should be `HIT` if `x-cacheable-on-cdn` is `true`. Visitors from different regions of the world may hit different CDN edge nodes, so a query cached in one part of the world may still cause a cache miss in other parts of the world.
    

For uncached requests hitting our origin (these will only be present if `cf-cache-status` is `MISS`), rate limits apply:

-   `x-ratelimit-limit`: Will return either `40` or `1000`, depending on which rate limit you're currently close to (40 per second or 1000 per minute). **Both** rate limits are always in effect, but this header tells you which one you're more at risk of.
-   `x-ratelimit-remaining`: How many requests you have remaining in the current bucket. For example, if the response's `x-ratelimit-limit` says `40` and `x-ratelimit-remaining` says 39, you have 39 requests left this second. If `x-ratelimit-limit` says 1000 and `x-ratelimit-remaining` is 999, you have 999 left for this minute.
    
-   `x-ratelimit-reset`: This header **only appears if you already hit a rate limit**, and indicates how long you should wait to retry again. It returns an integer in seconds, like `1` (for the per-second rate limit) or `59` (for the per-minute rate limit).
    

#### CDN Caching limitations for extremely large requests

If the body of your GraphQL request, after being compressed using gzip, exceeds 8KB in size, it will not be cached by our CDN. Consequently, these queries will be directed to the Content Delivery API origin servers, resulting in slower performance and being subjected to the rate limits mentioned above.

The efficiency of the gzip compression algorithm makes it highly unlikely that requests will exceed the 8KB limit. To give a rough estimate, 99% of our customers will never experience this limitation. However, it is important to be aware of this limitation and check the GraphQL queries if you notice a suspicious number of 429 status codes.

#### Reaching your plan monthly API calls limit

For projects under a paid plan, even exceeding the API calls or bandwidth limit does not lead to the interruption of the service, but the payment of an additional fee commensurate with the use. For projects under a free plan, service will be temporarily disabled until the beginning of the following calendar month, unless you provide a credit card.

For more details, check our [Plans, billing and pricing page](/docs/plans-pricing-and-billing.md).

---

# 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 — Pagination

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

Pagination allows you to efficiently retrieve large sets of records by breaking them into manageable chunks. This helps 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 then returns the next 5.

> [!NOTE] Edge cases
> If you request more records than exist, the query will return all available records. When the number of records is less than the requested amount, you'll receive the maximum available records.

### Calculating total number of results

To determine the total number of records and 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 result count will be different!

## Simplifying pagination

It is possible to automate the retrieval of a number of records that surpasses the 500-record limit without the need for manually handling the pagination logic by utilizing our GraphQL client `@datocms/cda-client`:

```typescript
import { executeQueryWithAutoPagination } from "@datocms/cda-client";

const { allQuotes } = await executeQueryWithAutoPagination(`
  query {
    # notice that we're fetching 5,000 records here!
    allArtists(first: 5000) { name }
  }
`, { ... });
```

You can find all the details in the documentation for the [`executeQueryWithAutoPagination`](https://github.com/datocms/cda-client/tree/main?tab=readme-ov-file#executequerywithautopagination) function.

---

# 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 — 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 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 — 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 — 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 — 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.

### 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
    }
  }
}
```

### 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 `_antivirusStatus`

`eq`

Search uploads with the specified antivirus status

```graphql
query {
  allProducts(filter: { _antivirusStatus: { eq: pending } }) {
    title
  }
}
```

`neq`

Exclude uploads with the specified antivirus status

```graphql
query {
  allProducts(filter: { _antivirusStatus: { neq: pending } }) {
    title
  }
}
```

`in`

Search uploads with the specified antivirus statuses

```graphql
query {
  allProducts(filter: { _antivirusStatus: { in: [pending] } }) {
    title
  }
}
```

`not_in`

Search uploads that do not have the specified antivirus statuses

```graphql
query {
  allProducts(filter: { _antivirusStatus: { notIn: [pending] } }) {
    title
  }
}
```

#### 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

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)

### 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

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.  Modify your existing GraphQL queries by adding a new `X-Cache-Tags` header;
    
2.  Tag your frontend pages with cache tags received from DatoCMS;
    
3.  Implement a specific endpoint to invalidate the tags that DatoCMS sends you via 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. Let's look at them in detail.

#### Step 1: Retrieve cache tags

Every response from the Content Delivery API has the capability to return a list of cache tags associated both with the query, and its results. To access these 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.

#### Step 2: Apply the tags to your website pages

This step **strongly depends on both the frontend framework and hosting solution you use**. However, the fundamental concept is that each artifact that your website produces (such as HTML pages, API responses, etc.) that uses content coming from DatoCMS, should be marked with the cache tags provided in the GraphQL response.

Read the section [**Integrating DatoCMS cache tags on your project**](/docs/content-delivery-api/cache-tags.md#integrating-datocms-cache-tags-on-your-project) below for more details.

#### Step 3: Implement the "Invalidate cache tag" webhook

After tagging your frontend artifacts, we need a method to invalidate them when necessary. 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.

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": []
}
```

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.

Just like Step 2, the ways in which you can perform cache invalidation through tags **greatly depend on the frontend framework and hosting solution you use**. In some instances, it's an API call, whereas some frameworks offer specific helper functions. Read the next section to learn more.

## Integrating DatoCMS cache tags on your project

Depending on the chosen stack, the practical implementation of cache tags can vary significantly, and in some instances, it may not be entirely feasible.

To aid you in navigating the possibilities, we can differentiate between two main paradigms:

### Case 1: Origin server + CDN

This first paradigm is more versatile and relies on well-known web standards.

If your website or application can define custom HTTP headers in the response on a per-page basis, then regardless of the specific language or framework used, you use DatoCMS Cache Tags by **placing a CDN on top of your website that supports Tag-Based Cache Invalidation.**

###### 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/). In a nutshell:

-   **Assign Tags:** When your application delivers a page, it can specify a series of tags in a specific response header (the header's name depends on the CDN). These tags serve as labels, that represent the content within that page.
-   **Caching:** The response is stored in the CDN cache with its primary cache key — the URL — plus the associated tags.
    
-   **Purging:** If any content linked to a particular tag is updated, instead of searching through all cached pages, the CDN can quickly identify and remove all items associated with that specific tag.
    

It's important to know that different services use different names for the same underlying concept technology. For example, Fastly refers to cache tags as "Surrogate Keys". The header with which your application can declare the tags to the CDN also varies depending on the service. With Netlify and Cloudflare, the name is `Cache-Tag`, while Bunny refers to it as `CDN-Tag`. What we in this documentation call *"cache invalidation,"* other services refer to as *"cache purge".*

Make sure to refer to the specific documentation of your CDN to know the details, format, and any potential limitations.

###### A practical example: Remix + Fastly ✨

To illustrate a combination of tools that fit into this category, we have put together a tutorial on implementing [**DatoCMS Cache Tags with Remix as the framework and Fastly**](/docs/remix/using-cache-tags.md) as the cache-tags-capable CDN on top of the Remix app.

### Case 2: Framework-centric approach

Some frameworks are created to protect the developer from the complexities of HTTP and architectural stack issues associated with tag-based caching methods. By using platform-specific adapters, they aim to handle all the implementation details for you. Developers are provided with a more abstract and general level of control over tag-based caching, in the form of helpers and functions that can function across different hosting environments.

###### A practical example: Next.js ✨

A prominent example in this category is Next.js, whose [`fetch()`](https://nextjs.org/docs/app/api-reference/functions/fetch#optionscache) and [`revalidateTag()`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag) function are the founding blocks for using cache tags, together with the framework inner logic.

We have covered in detail [**how to implement DatoCMS Cache Tags on a Next.js project**](/docs/next-js/using-cache-tags.md) in the relevant section of our documentation.

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

## Encoding of cache tags

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

---

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

###### `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/)

###### `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.

###### `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.

###### `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 — Technical Limits (CMA)

Source [docs]: https://www.datocms.com/docs/content-management-api/technical-limits.md

> [!NOTE] These are limits for the REST Content Management API
> For limits applicable to the GraphQL Content Delivery API, please instead see [Technical Limits (CDA)](/docs/content-delivery-api/technical-limits.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.

Here are the technical limits currently in place for the CMA:

-   **Maximum Record size**: 300 KB, including content in nested blocks (assets and linked records do not count toward the limit).
    
    -   *Please note that the maximum record size allowed by your plan may exceed the default 300KB limit. To confirm whether your plan supports a larger maximum record size, check the 'Plan and Billing' section in your Account dashboard.*
        
-   **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))
-   **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 rate limits specify the number of requests a client can make to the CMA in a specific time frame.

By default the Management API enforces rate limits of **60 requests per 3 seconds**. Higher rate limits may apply depending on your current plan.

In the following list you can find all the headers returned in every response by the Content Management API which give a client information on rate limiting:

-   `X-RateLimit-Limit`: the maximum amount of requests which can be made in 3 seconds.
-   `X-RateLimit-Remaining`: the remaining amount of requests which can be made until the next 3-seconds reset.
    
-   `X-RateLimit-Reset`: if present, indicates the number of seconds until the next request can be made.
    

When a client gets rate limited, the API responds with the `429 Too Many Requests` HTTP status code and sets the value `X-RateLimit-Reset` header to an integer larger than 0 specifying the time before the limit resets and another request will be accepted.

> [!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.

#### 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 }` |
| `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 }` |
| `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.

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"
```

## 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`), 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`) 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. 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

For each of the project's locales, the default metadata to apply if nothing is specified at record's level.

Example:

```json
{
  en: {
    title: "this is the default title",
    alt: "this is the default alternate text",
    custom_data: { foo: "bar" },
    focal_point: { x: 0.5, y: 0.5 },
  },
}
```

**`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: {
      en: {
        alt: "New default alt",
        title: "New default title",
        focal_point: {
          x: 0.3,
          y: 0.6,
        },
        custom_data: {
          watermark: true,
        },
      },
    },
  });

  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: {
    en: {
      alt: "new default alt",
      title: "new default title",
      focal_point: {
        x: 0.3,
        y: 0.6,
      },
      custom_data: {
        watermark: true,
      },
    },
  },
  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: {
    en: {
      alt: "new default alt",
      title: "new default title",
      focal_point: {
        x: 0.3,
        y: 0.6,
      },
      custom_data: {
        watermark: true,
      },
    },
  },
  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: {
      en: {
        alt: "New default alt",
        title: "New default title",
        focal_point: {
          x: 0.3,
          y: 0.6,
        },
        custom_data: {
          watermark: true,
        },
      },
    },
  });
}

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: {
    en: {
      alt: "new default alt",
      title: "new default title",
      focal_point: {
        x: 0.3,
        y: 0.6,
      },
      custom_data: {
        watermark: true,
      },
    },
  },
  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: {
    en: {
      title: "this is the default title",
      alt: "this is the default alternate text",
      custom_data: { foo: "bar" },
      focal_point: { x: 0.5, y: 0.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

For each of the project's locales, the default metadata to apply if nothing is specified at record's level.

Example:

```json
{
  en: {
    title: "this is the default title",
    alt: "this is the default alternate text",
    custom_data: { foo: "bar" },
    focal_point: { x: 0.5, y: 0.5 },
  },
}
```

**`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: {
    en: {
      title: "this is the default title",
      alt: "this is the default alternate text",
      custom_data: { foo: "bar" },
      focal_point: { x: 0.5, y: 0.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: {
    en: {
      title: "this is the default title",
      alt: "this is the default alternate text",
      custom_data: { foo: "bar" },
      focal_point: { x: 0.5, y: 0.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

For each of the project's locales, the default metadata to apply if nothing is specified at record's level.

Example:

```json
{
  en: {
    title: "this is the default title",
    alt: "this is the default alternate text",
    custom_data: { foo: "bar" },
    focal_point: { x: 0.5, y: 0.5 },
  },
}
```

**`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`**: Per-locale default metadata including alt text, title, focal point, and custom data

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: {
      en: {
        alt: "new default alt",
        title: "new default title",
        focal_point: {
          x: 0.3,
          y: 0.6,
        },
        custom_data: {},
      },
    },
  });

  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: {
    en: {
      alt: 'new default alt',
      title: 'new default title',
      focal_point: [Object],
      custom_data: {}
    }
  }
}
```


###### 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 widget

**`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

**`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

Specifies whether all users of this site need to authenticate using two-factor authentication

**`theme`**

- Type: object

Specifies the theme to use in administrative area

<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

**`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.

**`primary_color`**

- Type: object

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

**`green`**

- Type: integer
- Example: `128`

**`blue`**

- Type: integer
- Example: `128`

**`alpha`**

- Type: integer
- Example: `128`

</details>

**`light_color`**

- Type: object

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

**`green`**

- Type: integer
- Example: `128`

**`blue`**

- Type: integer
- Example: `128`

**`alpha`**

- Type: integer
- Example: `128`

</details>

**`accent_color`**

- Type: object

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

**`green`**

- Type: integer
- Example: `128`

**`blue`**

- Type: integer
- Example: `128`

**`alpha`**

- Type: integer
- Example: `128`

</details>

**`dark_color`**

- Type: object

<details>
<summary>Show object format</summary>

**`red`**

- Type: integer
- Example: `128`

**`green`**

- Type: integer
- Example: `128`

**`blue`**

- Type: integer
- Example: `128`

**`alpha`**

- Type: integer
- Example: `128`

</details>

**`logo`**

- Type: string, null
- Example: `"123"`

The upload ID that is used as the logo for the project

</details>

**`timezone`**

- Type: string
- Example: `"Europe/London"`

Site default timezone

**`ip_tracking_enabled`**

- Type: boolean

Specifies whether you want IPs to be tracked in the Project usages section

**`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.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`.

## 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,
    primary_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    light_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    accent_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    dark_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    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,
  },
  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

**`sso_default_role`**

- Optional
- Type: [ResourceLinkage\<"role"\>](https://www-draft.datocms.com/docs/content-management-api/resources/role.md)

## 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,
    primary_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    light_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    accent_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    dark_color: { red: 128, green: 128, blue: 128, alpha: 128 },
    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,
  },
  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

## 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

## 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)

## 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

**`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

**`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,
  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,
  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,
  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

**`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,
  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

## 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,
  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)

## 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,
  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

## 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,
  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,
  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,
  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

## 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

## 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.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,
    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: 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_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

**`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.

<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

**`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

## 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" },
};
```

---

# 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"],
  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"],
  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"],
  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,
  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

Whether 2-factor authentication is active for this account or not

**`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

**`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
- Example: `"never"`

When this API token was last used to access the Content Management API

<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
- Example: `"never"`

When this API token was last used to access the Content Delivery API

<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: object
- Example: `{ "X-Foo": "Bar" }`

Additional headers that will be sent

**`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

**`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

**`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

**`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: 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" },
}
```

**`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_url`**

- Type: 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)

**`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

**`webhook_token`**

- Type: string
- Example: `"xA1239ajsk123"`

Unique token for the webhook (it's the same token present in `webhook_url`)

<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_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_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_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_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_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

**`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

**`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

Suppose you upload this asset to one of your DatoCMS projects:

(Image content)

The URL that DatoCMS will assign to this image will be similar to this:

```plaintext
https://www.datocms-assets.com/205/1570696780-example.jpg
```

In fact, every asset URL will follow this structure:

```plaintext
https://www.datocms-assets.com/<project id>/<upload timestamp>-<asset name>.<original file format>
```

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

You can specify any number of parameters. The following URL, for example,

-   crops the image to be 800x500px, centering around the second face it recognizes inside the picture;
-   desaturates the image;
    
-   adds a copyright caption at the bottom;
-   transforms the format to be a PNG;
    

```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
```

This is the final result:

(Image content)

Even though the DatoCMS image backend (Imgix) is fast, you get a tremendous performance boost if your frontend limits the number of sizes and crops you ask for.

The first time the image is called with these parameters, Imgix will cache the resulting image on one of their geographically positioned CDN servers; subsequent calls with the same parameters will not need to reprocess the image. imgix will then propagate the image to all other CDN servers around the world, as shown on our [real-time map.](https://www.datocms.com/features/worldwide-cdn.md)

Take a look at [Imgix's Image API Reference](https://docs.imgix.com/apis/url) page to see all the transformations available.

### 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), 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)

When requesting a cropped version of an image without explicitly specifying a crop mode, DatoCMS will automatically center the crop on the focal point. This means that **99% of the time you won't have to change your code in any way**:

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

### 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 Event Coverage Liveblog

Try this demo »

](https://www.datocms.com/marketplace/starters/next-js-event-coverage-liveblog.md)[

(Image content)

Next.js Blog

Try this demo »

](https://www.datocms.com/marketplace/starters/nextjs-template-blog.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 — Limits and pricing

Source [docs]: https://www.datocms.com/docs/real-time-updates-api/rate-limiting.md

The Real-time Updates API is capable of supporting hundreds of thousands of concurrent connections.

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.

Generally, the number of requests made to the Content Delivery API is equal to the number of [update events](/docs/real-time-updates-api/api-reference.md#receiving-the-events) received on a channel. However, **the count of requests is largely independent of the number of connected clients**, as long as they are all connected to exactly the same GraphQL query, with the same API token, on the same environment and draft/published version.

```plaintext
Requests made to CDA/minute = Rate of updates/minute * N
```

Where `N` is proportional to the overall number of open connections (the greater the number of connections, the higher it will be), but **is generally between 2 and 10**.

#### Example

Let's illustrate the count by using an example:

-   the home page of a site makes a connection to the Real-time Updates API;
-   the GraphQL query and API token are hard-coded in the code of the webpage, so they're the same for all;
    
-   the content of the page is modified by editors approx. once every 30 seconds;
-   a total of 500 users is connected at the same time to the homepage in question.
    

The number of API calls made to the Content Delivery API every minute will be between 4 and 20.

---

# 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. Sign in once via OAuth and work across every project you have access to in a single session — no API tokens to copy, no local credentials to manage, nothing to install.

The DatoCMS MCP server acts as a bridge between AI assistants and the [DatoCMS Content Management API](/docs/content-management-api.md), allowing large language models (LLMs) to interact with your DatoCMS projects in a structured, secure, and standardized way. Unlike traditional MCP servers that simply expose raw API endpoints, this server uses a layered tool design that guides LLMs through discovery, planning, and execution stages, significantly improving success rates and reliability.

(Video content)

Remote MCP on Claude Desktop

## 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
-   **Explore the API**: Discover available resources, actions, and methods with complete documentation
    
-   **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
    
-   **Write and run scripts**: Create TypeScript scripts that batch multiple operations together, executed in isolated sandboxes for security
    

Users report excellent success rates for operations like:

-   Create new records with content across multiple fields
-   Upload and attach images to records
    
-   Add translations to existing records
-   Link records together (establishing relationships)
    
-   Update content across multiple records
-   Modify your content schema
    

Most operations complete fast and accurately, with occasional minor refinements needed (which can be requested in follow-up prompts).

## Requirements

-   A DatoCMS account (you will authenticate via OAuth on first connection)
-   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 sandbox 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

To keep the service reliable for everyone, scripts that the AI executes inside the sandbox 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 |
| Weekly sandbox time budget per account | 10 minutes | 90 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.
-   **Weekly time budget**: every second a script spends running in the sandbox counts against the account's weekly budget. The budget resets on a rolling weekly window. Discovery and documentation tools (`get_api_methods`, `get_schema`, etc.) do not consume sandbox 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

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
    
-   Tokens can be revoked at any time from your DatoCMS account settings; the server detects revoked or expired tokens and prompts a re-authentication
    

###### Sandboxed script execution

Every script the AI writes runs in an isolated sandbox 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 sandbox 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 sandbox 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 weekly time budgets** prevent runaway loops and abuse
    

###### Script validation

Beyond the sandbox, 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 weekly sandbox 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.

---

# Environments and migrations — Introduction to Environments & Migrations

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**](/docs/scripting-migrations/apply-migrations-to-primary-environment.md#step-1-turn-on-maintenance-mode-to-prevent-changes-to-the-primary-environment), 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 — Configuring the CLI

Source [docs]: https://www.datocms.com/docs/scripting-migrations/installing-the-cli.md

First of all, you need to install the DatoCMS CLI with the following command:

Terminal window

```bash
npm install datocms
```

You can verify that everything is correctly installed by running the help command of the CLI:

Terminal window

```bash
npx datocms --help
```

### Authenticating with the CLI

The recommended way to authenticate is via OAuth. Run the following command to log in with your DatoCMS account:

Terminal window

```bash
npx datocms login
```

This opens your browser for a secure login flow. Credentials are stored locally at `~/.config/datocms/credentials.json`. You can verify your identity at any time with:

Terminal window

```bash
npx datocms whoami
```

### Linking a project

Next, link the current directory to your DatoCMS project. This generates a `datocms.config.json` configuration file 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 will automatically resolve 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": ""
      }
    }
  }
}
```

Once done, add the config file to your Git repository:

Terminal window

```bash
git add datocms.config.json
git commit -m "Add datocms.config.json file"
```

> [!POSITIVE] Need to manage multiple DatoCMS projects from the same repo?
> You can set up additional profiles with the `datocms link --profile=<NEW_PROFILE_NAME>` command.
> 
> When you have multiple profiles, you can specify the profile to use to run a command with the `--profile` flag (or by exposing a `DATOCMS_PROFILE` environment variable).

### Alternative: Specify a DatoCMS API token

If you prefer not to use OAuth login (e.g. in CI/CD pipelines), you can still provide an API token directly. The CLI resolves authentication in this order:

1.  `--api-token` flag
    
2.  environment variable
    
3.  linked project via OAuth.
    

The API token's associated role needs at least these permissions on all the environments you want to migrate from or to:

-   Customize content navigation bar
-   Create/edit models and plugins
    
-   Create/edit workflows
-   Create/edit shared filters
    

(Image content)

You can pass the API token as a parameter to every command, e.g.:

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` file, so you can also place the token there — but make sure not to commit the file to your repo!

Terminal window

```bash
$ echo '.env' >> .gitignore
$ echo 'DATOCMS_API_TOKEN=<YOUR-API-TOKEN>' >> .env
```

---

# 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/scripting-migrations/installing-the-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'
```

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](/docs/scripting-migrations/apply-migrations-to-primary-environment.md#step-1-turn-on-maintenance-mode-to-prevent-changes-to-the-primary-environment).

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 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
```

---

# 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).

---

# 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 AWS S3 details:

-   **S3 Bucket Name**
-   **AWS Region**
    
-   **S3 Access Key ID**
-   **S3 Secret Access Key**
    

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!

---

# 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>
  );
}
```

# Color palette CSS variables

Within the `Canvas` component, a color palette is made available as a set of CSS variables, allowing you to conform to the theme of the current environment:

Preview

Code

```js
<Canvas ctx={ctx}>
  <Section title="Text colors">
    <table>
      <tbody>
        <tr>
          <td>
            <code>--base-body-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--base-body-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--light-body-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--light-body-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--placeholder-body-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--placeholder-body-color)',
              }}
            />
          </td>
        </tr>
      </tbody>
    </table>
  </Section>
  <Section title="UI colors">
    <table>
      <tbody>
        <tr>
          <td>
            <code>--light-bg-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--light-bg-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--lighter-bg-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--lighter-bg-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--disabled-bg-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--disabled-bg-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--border-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--border-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--darker-border-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--darker-border-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--alert-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--alert-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--warning-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--warning-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--notice-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--notice-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--warning-bg-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--warning-bg-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--add-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--add-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--remove-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--remove-color)',
              }}
            />
          </td>
        </tr>
      </tbody>
    </table>
  </Section>
  <Section title="Project-specific colors">
    <table>
      <tbody>
        <tr>
          <td>
            <code>--accent-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--accent-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--primary-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--primary-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--light-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--light-color)',
              }}
            />
          </td>
        </tr>
        <tr>
          <td>
            <code>--dark-color</code>
          </td>
          <td width="30%">
            <div
              style={{
                width: '30px',
                height: '30px',
                background: 'var(--dark-color)',
              }}
            />
          </td>
        </tr>
      </tbody>
    </table>
  </Section>
</Canvas>
```

# Typography CSS variables

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}>
  <table>
    <tbody>
      <tr>
        <td>
          <code>--font-size-xxs</code>
        </td>
        <td>
          <div style={{ fontSize: 'var(--font-size-xxs)' }}>
            Size XXS
          </div>
        </td>
      </tr>
      <tr>
        <td>
          <code>--font-size-xs</code>
        </td>
        <td>
          <div style={{ fontSize: 'var(--font-size-xs)' }}>Size XS</div>
        </td>
      </tr>
      <tr>
        <td>
          <code>--font-size-s</code>
        </td>
        <td>
          <div style={{ fontSize: 'var(--font-size-s)' }}>Size S</div>
        </td>
      </tr>
      <tr>
        <td>
          <code>--font-size-m</code>
        </td>
        <td>
          <div style={{ fontSize: 'var(--font-size-m)' }}>Size M</div>
        </td>
      </tr>
      <tr>
        <td>
          <code>--font-size-l</code>
        </td>
        <td>
          <div
            style={{
              fontSize: 'var(--font-size-l)',
              fontWeight: 'var(--font-weight-bold)',
            }}
          >
            Size L
          </div>
        </td>
      </tr>
      <tr>
        <td>
          <code>--font-size-xl</code>
        </td>
        <td>
          <div
            style={{
              fontSize: 'var(--font-size-xl)',
              fontWeight: 'var(--font-weight-bold)',
            }}
          >
            Size XL
          </div>
        </td>
      </tr>
      <tr>
        <td>
          <code>--font-size-xxl</code>
        </td>
        <td>
          <div
            style={{
              fontSize: 'var(--font-size-xxl)',
              fontWeight: 'var(--font-weight-bold)',
            }}
          >
            Size XXL
          </div>
        </td>
      </tr>
      <tr>
        <td>
          <code>--font-size-xxxl</code>
        </td>
        <td>
          <div
            style={{
              fontSize: 'var(--font-size-xxxl)',
              fontWeight: 'var(--font-weight-bold)',
            }}
          >
            Size XXXL
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</Canvas>
```

# Spacing CSS variables

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}>
  <table>
    <tbody>
      <tr>
        <td>
          <code>--spacing-s</code>
        </td>
        <td>
          <div
            style={{
              background: 'var(--accent-color)',
              width: 'var(--spacing-s)',
              height: 'var(--spacing-s)',
            }}
          />
        </td>
      </tr>
      <tr>
        <td>
          <code>--spacing-m</code>
        </td>
        <td>
          <div
            style={{
              background: 'var(--accent-color)',
              width: 'var(--spacing-m)',
              height: 'var(--spacing-m)',
            }}
          />
        </td>
      </tr>
      <tr>
        <td>
          <code>--spacing-l</code>
        </td>
        <td>
          <div
            style={{
              background: 'var(--accent-color)',
              width: 'var(--spacing-l)',
              height: 'var(--spacing-l)',
            }}
          />
        </td>
      </tr>
      <tr>
        <td>
          <code>--spacing-xl</code>
        </td>
        <td>
          <div
            style={{
              background: 'var(--accent-color)',
              width: 'var(--spacing-xl)',
              height: 'var(--spacing-xl)',
            }}
          />
        </td>
      </tr>
      <tr>
        <td>
          <code>--spacing-xxl</code>
        </td>
        <td>
          <div
            style={{
              background: 'var(--accent-color)',
              width: 'var(--spacing-xxl)',
              height: 'var(--spacing-xxl)',
            }}
          />
        </td>
      </tr>
      <tr>
        <td>
          <code>--spacing-xxxl</code>
        </td>
        <td>
          <div
            style={{
              background: 'var(--accent-color)',
              width: 'var(--spacing-xxxl)',
              height: 'var(--spacing-xxxl)',
            }}
          />
        </td>
      </tr>
    </tbody>
  </table>
</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(--border-color)',
      }}
    >
      <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(--light-bg-color)',
      }}
    >
      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(--light-bg-color)',
      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(--light-bg-color)',
      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(--light-bg-color)',
      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(--border-color)' }}>
        <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(--border-color)' }}>
        <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(--border-color)',
             }}
           >
             <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(--border-color)',
             }}
           >
             <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 — 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.previewImage`: Relative path to a video/image showing the plugin in action (better if it's a MP4 video)
    
-   `datoCmsPlugin.coverImage`: Relative path to a cover image that will be used in the [Marketplace](https://www.datocms.com/marketplace/plugins.md)
-   `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.

### 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)

## 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,
    });
  },
});
```

---

# 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/Remix](/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 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>
```

---

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

---

# Pro tips — DatoCMS Pro Tips

Source [docs]: https://www.datocms.com/docs/pro-tips.md

This is a series of "pro-tips", small tutorials to explain little but powerful features of DatoCMS, hope you find them useful!

---

# Pro tips — Customize CMS domain

Source [docs]: https://www.datocms.com/docs/pro-tips/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.

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

(Video content)

---

# Pro tips — How to manage a live and a preview site

Source [docs]: https://www.datocms.com/docs/pro-tips/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.

---

# 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:**

How to create a GraphQL API Token (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.

How to create a GraphQL API Token (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".

How to create a GraphQL API Token (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".

How to create a GraphQL API Token (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.

---

# Remix — Remix + DatoCMS Overview

Source [docs]: https://www.datocms.com/docs/remix/get-started.md

Remix is an exceptional tool for building modern 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 from which you can build upon.

Remix fully supports edge functions and advanced caching mechanisms, and Remix projects can be deployed on many different hostings, such as [Netlify](https://netlify.com/), [Vercel](https://vercel.com/solutions/nextjs) and [Cloudflare Pages](https://pages.cloudflare.com/).

DatoCMS is the perfect companion to Remix 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.**

Our [marketplace](https://www.datocms.com/marketplace/starters.md) features different demo projects on Remix, so you can learn and get started easily:

[

(Image content)

Remix Blog

Try this demo »

](https://www.datocms.com/marketplace/starters/remix-blog-example-template.md)

### Fetching content from our GraphQL API

First, use the Remix wizard to set up a new project. Read more about your options on the [Remix docs](https://remix.run/docs/en/v1#getting-started).

Terminal window

```bash
npx create-remix@latest
```

The way you fetch content from external sources in Remix is by exporting a `loader` function from your route files. Inside the React component you can then retrieve that data with a special hook called `useLoaderData`:

app/routes/index.jsx

```javascript
import { useLoaderData } from 'remix';

export async function loader() => {
  return { foo: 'bar' };
};

export default Homepage(props) {
  const { foo } = useLoaderData();

  // ...
}
```

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`, and inside of it, add a file called `datocms.js`:

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. Read [this tutorial](https://remix.run/docs/en/v1/guides/envvars#server-environment-variables) to know more on how to set server environment variables in Remix.

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.

How to create a GraphQL API Token (Video content)

It's time to use our function in a real page! Open up `app/routes/index.jsx` — which is the route that renders the homepage — and define the `loader` function and a basic page component:

```jsx
import { useLoaderData } from "remix";
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() {
  const { posts } = useLoaderData();

  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!

---

# Remix — Managing images

Source [docs]: https://www.datocms.com/docs/remix/remix-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 Remix 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.

```jsx
import { load } from "~/lib/datocms";
import { Image } from "react-datocms";
import { useLoaderData } from "remix";

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() {
  const { posts } = useLoaderData();

  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;

---

# Remix — Displaying videos

Source [docs]: https://www.datocms.com/docs/remix/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:

```jsx
import { load } from "~/lib/datocms";
import { VideoPlayer } from "react-datocms";
import { useLoaderData } from "remix";

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() {
  const { posts } = useLoaderData();
  return (
    <div>
      {posts.map(blogPost => (
        <article key={blogPost.id}>
          <VideoPlayer data={blogPost.coverVideo.video} />
          <h6>{blogPost.title}</h6>
        </article>
      ))}
    </div>
  );
}
```

---

# Remix — Structured Text fields

Source [docs]: https://www.datocms.com/docs/remix/remix-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 Remix 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:

```jsx
import { load } from "~/lib/datocms";
import { StructuredText } from "react-datocms";
import { useLoaderData } from "remix";

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() {
  const { posts } = useLoaderData();

  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;
      }
    }}
  />
);
```

---

# Remix — Adding SEO to pages

Source [docs]: https://www.datocms.com/docs/remix/add-seo-to-remix.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 Remix 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 Remix, you can specify meta tags for a page using the [`meta` export](https://remix.run/docs/en/v1/api/conventions#meta):

```javascript
export function meta({ data }) {
    title: "Something cool",
    description: "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 Remix by using the `toRemixMeta` function, which generates a compatible object for Remix's `meta` function:

```jsx
import { request } from "../lib/datocms";
import { toRemixMeta } from "react-datocms";
import { useLoaderData } from "remix";

const HOMEPAGE_QUERY = `
  {
    blog {
      seo: _seoMetaTags {
        attributes
        content
        tag
      }
    }
  }`;

export async function loader() {
  return request(HOMEPAGE_QUERY, {
    variables: { limit: 10 }
  });
}

export function meta({ data }) {
  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?
> Remix offers a [`links`](https://remix.run/docs/en/v1.1.1/api/conventions#links) export to define which `<link>` elements to add to the page, but for performance reasons [it doesn't receive any loader data](https://github.com/remix-run/remix/issues/32), so you cannot use it to render favicons meta tags! The best way to render them is using `renderMetaTags` in your root component, like in the example.

```jsx
import {
  Links,
  LiveReload,
  Meta,
  Form,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData,
} from 'remix';
import { renderMetaTags } from 'react-datocms';
import { load } from '~/lib/datocms';

export const loader = async ({ request }) => {
  return load(`
    {
      site: _site {
        favicon: faviconMetaTags(variants: [icon, msApplication, appleTouchIcon]) {
          attributes
          content
          tag
        }
      }
    }
  `);
};

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 />
        <ScrollRestoration />
        <Scripts />
        {process.env.NODE_ENV === 'development' && <LiveReload />}
      </body>
    </html>
  );
}
```

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)

---

# Remix — Setting up a preview mode

Source [docs]: https://www.datocms.com/docs/remix/setting-up-a-preview-mode-with-remix.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 Remix 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 API routes to turn Preview mode on/off

First, we need to create a couple of [API routes](https://remix.run/docs/en/v1.1.1/guides/api-routes) to enable/disable Preview Mode. We're going to use Remix's [built-in session management](https://remix.run/docs/en/v1/api/remix#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.js`:

```javascript
import { createCookieSessionStorage } from "remix";

const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    cookie: {
      name: "__session",
      maxAge: 604_800,
      path: '/',
    }
  });

export { getSession, commitSession, destroySession };
```

Now we can use it inside a new API route under `app/routes/preview/start.js`, that we can call to turn on the preview mode:

```javascript
import { redirect } from 'remix';
import { getSession, commitSession } from '~/sessions';

export const action = async ({ request }) => {
  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.js`, to turn preview mode off:

```javascript
import { redirect } from 'remix';
import { getSession, commitSession } from '~/sessions';

export const action = async ({ request }) => {
  const session = await getSession(request.headers.get('Cookie'));

  session.unset('preview');

  return redirect('/', {
    headers: {
      'Set-Cookie': await commitSession(session),
    },
  });
};
```

We can now tweak the `app/root.jsx` file to add to every page a button to toggle the preview on and off:

```javascript
import { getSession } from '~/sessions';

export const loader = async ({ request }) => {
  const session = await getSession(request.headers.get('Cookie'));
  return { previewEnabled: session.has('preview') };
};

export default function App() {
  const { previewEnabled } = useLoaderData();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {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>
        )}
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        {process.env.NODE_ENV === 'development' && <LiveReload />}
      </body>
    </html>
  );
}
```

### 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:

```javascript
import { useLoaderData } from "remix";
import { load } from "~/lib/datocms";

const HOMEPAGE_QUERY = `query HomePage($limit: IntType) {
  posts: allBlogPosts(first: $limit) {
    title
  }
}`;

export async function loader({ request }) => {
  const session = await getSession(request.headers.get('Cookie'));

  return load(HOMEPAGE_QUERY, {
    variables: { limit: 10 }
    includeDrafts: session.has('preview'),
  });
};

export default function Home() {
  const { posts } = useLoaderData();

  return <div>{JSON.stringify(posts, null, 2)}</div>;
}
```

---

# Remix — Real-time updates

Source [docs]: https://www.datocms.com/docs/remix/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 Remix 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 Remix 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:

```jsx
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 Home({ subscription }) {
  const { data, error, status } = useQuerySubscription(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/remix/setting-up-a-preview-mode-with-remix.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 `context.preview` is true), 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` prop with just `initialData` and `enabled: false` options, without any additional clutter.
    

Here's an example snippet:

```jsx
import { load } from '~/lib/datocms';

const BLOG_POST_QUERY = `
  query HomePage {
    blogPost {
      title
    }
  }
`;

export async function loader({ request }) {
  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,
      };
  };
}
```

If you want to directly see the final result, we've prepared a [fully working Next.js blog](https://next-event-coverage-liveblog.vercel.app/), with real-time updates of draft content in Preview Mode:

Remix Blog

(Image content)

Remix Blog

Publish this demo online with just three clicks in a matter of minutes.

[Deploy the demo project](https://dashboard.datocms.com/deploy?repo=datocms/remix-example:main) (Image content)

---

# Remix — DatoCMS Cache Tags and Remix

Source [docs]: https://www.datocms.com/docs/remix/using-cache-tags.md

DatoCMS and Remix make the perfect couple to provide a great user experience: using DatoCMS [Cache Tags](/docs/content-delivery-api/cache-tags.md), you can build websites, cache pages on a CDN for maximum performance, and don't worry about cache invalidation!

> [!WARNING]
> To take advantage of this technique, it is necessary to pair your Remix application with a [CDN capable of managing caching via tags](/docs/content-delivery-api/cache-tags.md#what-is-tag-based-cache-invalidation)! The CDN should be positioned above your Remix application, directly serving the visitors.

The whole recipe is made of two parts:

-   Obtain cache tags from DatoCMS and use them to instruct the CDN;
-   Invalidate cache entries through the use of a webhook.
    

## Step 1: Retrieve and apply cache tags

By adding a `X-Cache-Tags: true` header into your usual Content Delivery API GraphQL queries, the response will include a set of related cache tags in the `X-Cache-Tags` header:

```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 } }" }'

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": {
    ...
  }
}
```

> [!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.

The actual code to use to perform your queries should be something like this:

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

export async function executeQuery(query, options) {
  const [data, response] = await rawExecuteQuery(
    query,
    {
      ...options,
      returnCacheTags: true,
    },
  );

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

  return { data, cacheTags };
}
```

We've highlighted two elements in the code above:

-   with `returnCacheTags`, we set `X-Cache-Tags: true` instructing DatoCMS to return cache tags;
-   once the API responds, we retrieve cache tags with `response.headers.get("x-cache-tags")`.
    

Then, we return the data from the GraphQL query and the cache tags string.

Once we have this function to fetch content from DatoCMS, we need to export two functions from the Remix route files where we want to support cache tags, `loader()` and `headers()`:

```javascript
import { json } from "@remix-run/node";
import { executeQuery } from "lib/fetch-contents";

export const loader = async () => {
  const { data, cacheTags } = await executeQuery(SOME_GRAQHQL_QUERY);

  return json(
    { data },
    {
      headers: cacheTags
        ? {
            "Surrogate-Key": cacheTags,
            "Surrogate-Control": "max-age=31536000",
          }
        : {},
    }
  );
};

export const headers = ({ loaderHeaders }) => {
  const headers = new Headers();

  for (const header of ["surrogate-key", "surrogate-control"]) {
    const value = loaderHeaders.get(header);

    if (value) {
      headers.set(header, value);
    }
  }

  return headers;
};
```

The `loader()` function instructs Remix on how to fetch the data required to generate a page: we utilize the `json()` helper to return the result of our query so that it's available in our React component, but most importantly, we pass the headers options to configure how this data will be cached by the CDN (in our case, Fastly):

1.  `Surrogate-Control` instructs Fastly to cache this data for a year;
    
2.  `Surrogate-Key` instructs Fastly to mark this response with the tags coming from DatoCMS.
    

The `headers()` function is used to specify the headers that will be associated not with the data, but with the actual page. Instead of repeating a new query to DatoCMS, we take the headers we just returned from the loader, and set them as part of the response.

> [!WARNING] Headers can change depending on your CDN!
> Different CDNs use different names for the same concepts.
> 
> What we call *cache tags* are *surrogate keys* among some providers (like for Fastly, which we're using in this example) ; instead of *invalidate*, many use *purge*. Examples: [Netlify](https://docs.netlify.com/platform/caching/#purge-by-cache-tag), [Cloudflare](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/), [Fastly](https://docs.fastly.com/en/guides/purging-with-surrogate-keys).
> 
> Similarly, the names of the headers, or the format of the associated value, change from service to service: be sure to check the exact header name in the provider's documentation. Some examples:
> 
> -   [Fastly](https://docs.fastly.com/en/guides/working-with-surrogate-keys) uses `Surrogate-Key` with a space-separated list of tags;
>     
> -   [CloudFlare](https://developers.cloudflare.com/cache/how-to/purge-cache/purge-by-tags/#add-cache-tag-http-response-headers) uses the `Cache-Tag` header with comma-separated tags;
>     
> -   [Netlify](https://docs.netlify.com/platform/caching/#add-cache-tags) has Netlify-Cache-Tag with a comma-separated tag string.
>     
> 
> Also, be mindful of potential constraints regarding the length of the header. We strive to minimize tags as much as we can (for instance, we utilize an alphabet of 83 symbols), but the quantity and size of tags are contingent on the query.

## Step 2: Implement the "Invalidate cache tag" webhook

After tagging the responses, it's time to see how you can invalidate the cache when editors change content. First, inside your DatoCMS project Settings, create a new webhook and set as trigger the "Invalidate" event of the "Content Delivery API Cache Tags" entity:

(Video content)

When editors change content, DatoCMS will send a webhook containing all the cache tags that must be invalidated. The webhook request looks like this:

```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": []
}
```

To process this request, you need to add an API endpoint in Remix that receives it and calls the CDN to request the invalidation of the cache associated with the tags:

```typescript
import { json } from "@remix-run/node";

async function invalidateFastlySurrogateKeys(serviceId, fastlyKey, keys) {
  return fetch(`https://api.fastly.com/service/${serviceId}/purge`, {
    method: "POST",
    headers: {
      "fastly-key": fastlyKey,
      "content-type": "application/json",
    },
    body: JSON.stringify({ surrogate_keys: keys }),
  });
}

export const action = async ({ request }) => {
  if (request.method !== "POST") {
    return json({ success: false }, 404);
  }

  if (
    request.headers.get("authorization") !==
    `Bearer ${process.env.CACHE_INVALIDATION_WEBHOOK_TOKEN}`
  ) {
    return json({ success: false }, 401);
  }

  const body = await request.json();

  const { tags } = body.entity.attributes;

  const response = await invalidateFastlySurrogateKeys(
    process.env.FASTLY_SERVICE_ID,
    process.env.FASTLY_KEY,
    tags
  );

  if (!response.ok) {
    const responseBody = await response.json();

    return json(responseBody, response.status);
  }

  return json({ success: true }, response.status);
};
```

> [!WARNING] The method of invalidating the cache varies depending on your CDN!
> The example above is again based on Fastly: depending on the service you're using, you'll have to use a slightly different method for invalidating the cache. Some examples:
> 
> -   [How to purge via surrogate keys on Fastly](https://docs.fastly.com/en/guides/purging-with-surrogate-keys)
>     
> -   [Cloudflare approach](https://developers.cloudflare.com/api/operations/zone-purge)
>     
> -   [Netlify's way](https://docs.netlify.com/platform/caching/#purge-by-cache-tag)

#### Invalidating on deploy

Even though it's not specifically related to the use of our Cache Tags, it's important to remember that when there's a cache layer above your application, you need to worry about invalidating this cache not only when incoming content from DatoCMS changes — as we did during this tutorial — but also when a new version of your application is deployed!

Fortunately, this usually happens much less frequently compared to a content change, and therefore it is often sufficient to handle this situation with a complete invalidation of the CDN cache at each deployment.

---

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

---

# 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 cancel at any time during your billing cycle at no cost. To cancel a subscription you can either downgrade your subscription to the free plan or simply delete the account or organization holding the subscription. To downgrade you might need to delete or reduce the usages of your projects.

When cancelling the subscription any overage is tallied and charged immediately.

You can request a refund for a plan within 15 days of purchase. Refunds are issued to the card that was originally charged and may take up to 10 business days to process.

If you are not sure how long do you need the subscription for, then use the monthly subscription, it's normally more expensive but you get more flexibility to cancel.

---

# 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).

---

# 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 11 projects (inclusive of one project in the regular price and the option to add 10 extra projects for €29/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.

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

### 🤖 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
    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.

## 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.34/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v6.2.32/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v5.4.46/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/v4.0.27/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/v5.4.46/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/v5.4.46/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/v5.4.46/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/v5.4.46/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/v5.4.46/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/v5.4.46/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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/v4.0.27/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] 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>      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/v4.0.25/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:

```
CONTENTFUL_TOKEN=xxx npm run 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] 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>      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/v4.0.25/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 run 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
```

---

# 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` 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 'react-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 (strings, objects, arrays, primitives):

```typescript
import { stripStega } from 'react-datocms';

// 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';

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)
- [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
```

---

# 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)
- [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
```

---

# 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 (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 |

---

# 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                                 |
| 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)
- [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
```

---

# 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)
- [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                                              | 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

```svelte
<script>
import { querySubscription } from 'react-datocms';

const subscription = useQuerySubscription({
  enabled: true,
  query: `
    query AppQuery($first: IntType) {
      allBlogPosts {
        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>
  </ul>
{/if}
```

---

# 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) to 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.

## 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 instead takes a document generated with `parse5`.

```js
import parse5 from 'parse5';
import { parse5ToStructuredText } from 'datocms-html-to-structured-text';

parse5ToStructuredText(
  parse5.parse(html, {
    sourceCodeLocationInfo: true,
  }),
).then((structuredText) => {
  console.log(structuredText);
});
```

Internally, both utilities work on a [Hast](https://github.com/syntax-tree/hast). Should you have a `hast` already you can use a third utility called `hastToDast`.

## 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 to validate every `dast` to avoid errors later when creating records.

## Advanced Usage

### Options

All the `*ToStructuredText` utils accept an optional `options` object as second argument:

```js
type Options = Partial<{
  newlines: boolean,
  // Override existing `hast` node handlers or add new ones
  handlers: Record<string, CreateNodeFunction>,
  // Allows to tweak the `hast` tree before transforming it to a `dast` document
  preprocess: (hast: HastRootNode) => HastRootNode,
  // 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>,
}>;
```

### Transforming Nodes

The utils in this library traverse a `hast` tree and transform supported nodes to `dast` nodes. The transformation is done by working on a `hast` node with a handler (async) function.

Handlers are associated to `hast` nodes by `tagName` or `type` when `node.type !== 'element'` and look as follow:

```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](./handlers.ts) for examples.

The default handlers are available on `context.defaultHandlers`.

### context

Every handler receives a `context` object that includes the following information:

```js
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. This is used for resolving relative URLs.
  baseUrl?: string;
}

export interface Context {
  // The current parent `dast` node type.
  parentNodeType: NodeType;
  // The parent `hast` node.
  parentNode: HastNode;
  // A reference to the current handlers - merged default + user handlers.
  handlers: Record<string, Handler<unknown>>;
  // A reference to the default handlers record (map).
  defaultHandlers: Record<string, Handler<unknown>>;
  // 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;
  // Array of allowed Block types.
  allowedBlocks: Array<
    BlockquoteType | CodeType | HeadingType | LinkType | ListType,
  >;
  // Array of allowed marks.
  allowedMarks: Mark[];
  // Properties in this object are available to every handler as Context
  // is not deeply cloned.
  global: GlobalContext;
}
```

### Custom Handlers

It is possible to register custom handlers and override the default behavior via options:

```js
import { paragraphHandler } from './customHandlers';

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 generating a tree that is compliant with DatoCMS' Structured Text.

## preprocessing

Because of the strictness of the `dast` spec it is possible that some semantic or elements might be lost during the transformation.

To improve the final result, you might want to modify the `hast` before it is transformed to `dast` with the `preprocess` hook.

```js
import { findAll } from 'unist-utils-core';
const html = `
  <p>convert this to an h1</p>
`;

htmlToStructuredText(html, {
  preprocess: (tree) => {
    // Transform <p> to <h1>
    findAll(tree, (node) => {
      if (node.type === 'element' && 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 presented as `Block` nodes but these are not allowed inside of `ListItem` nodes (ul/ol lists). In this example we will split the list in 3 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 { visit } from 'unist-utils-core';

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();
    const body = find(tree, (node) => node.tagName === 'body');

    visit(body, (node, index, parents) => {
      if (
        !node ||
        node.tagName !== 'img' ||
        liftedImages.has(node) ||
        parents.length === 1 // is a top level img
      ) {
        return;
      }
      // remove image

      const imgParent = parents[parents.length - 1];
      imgParent.children.splice(index, 1);

      let i = parents.length;
      let splitChildrenIndex = index;
      let childrenAfterSplitPoint = [];

      while (--i > 0) {
        // Example: i == 2
        // [ 'body', 'div', 'h1' ]
        const /* h1 */ parent = parents[i];
        const /* div */ parentsParent = parents[i - 1];

        // Delete the siblings after the image and save them in a variable
        childrenAfterSplitPoint /* [ 'h1.2' ] */ = parent.children.splice(
          splitChildrenIndex,
        );
        // parent.children is now == [ 'h1.1' ]

        // parentsParent.children = [ 'h1' ]
        splitChildrenIndex = parentsParent.children.indexOf(parent);
        // splitChildrenIndex = 0

        let nodeInserted = false;

        // If we reached the 'div' add the image's node
        if (i === 1) {
          splitChildrenIndex += 1;
          parentsParent.children.splice(splitChildrenIndex, 0, node);
          liftedImages.add(node);

          nodeInserted = true;
        }

        splitChildrenIndex += 1;
        // Create a new branch with childrenAfterSplitPoint if we have any i.e.
        // <h1>h1.2</h1>
        if (childrenAfterSplitPoint.length > 0) {
          parentsParent.children.splice(splitChildrenIndex, 0, {
            ...parent,
            children: childrenAfterSplitPoint,
          });
        }
        // Remove the parent if empty
        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 { visit, CONTINUE } from 'unist-utils-core';

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) => {
    visit(tree, (node, index, parents) => {
      if (node.tagName === 'img' && parents.length > 1) {
        const parent = parents[parents.length - 1];
        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 using the [unist-utils-core](https://www.npmjs.com/package/unist-utils-core) library.

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