Prevent Unpublishing of Singleton Models

It would be great if there were an option to prevent unpublishing of some singleton models.

Often I use a singleton model to describe a page with fixed layout, and I pull the relevant variable into a static site generator (eg. on my contact.html.erb page I may use dato.contact_page).

I want these pages to use the publishing workflow so that my users can test out changes on staging before deploying to production. But if they unpublish one, the static site generator build will fail (unless I add a load of code to conditionally generate the pages, both per-page and in terms of navigation menu links and cross-page links, which as well as being time consuming and being a big potential source of bugs, doesn’t make sense as these pages are never meant to be unpublished).

It occurred to me that this could also be achieved by making ‘publish’ and ‘unpublish’ separate items in the role permissions section. Though you’d have to add a rule for every singleton you wanted to prevent publishing on.

1 Like

Another idea, probably much better than the above two: continue allowing users to unpublish singleton models as they please, but have the Dato gem just not throw errors when accessing fields on an unpublished singleton. You could then check a model’s published status and tell your site generator whether or not the page is to be generated.

So I guess this is more of a request for the Ruby library than a Dato request —

  • It would be great if accessing fields on an unpublished singleton model didn’t throw errors
  • It would be great if the ItemStatus from the GraphQL API was exposed to the Ruby library to check if an item is published (I couldn’t seem to find status, or published_at, or first_published_at in the Ruby library).
1 Like

Thank you for this. We’ll discuss your suggestions internally and see what we can do.

Hi @webworkshop the method contact_page is not created by the client if the page is unpublished, so to prevent your static site generator build to fail in this situation you should use

if dato.contact_page 
  create_post "src/contact.md" do
  frontmatter :yaml, {...}
end

To access the record’s status info call the method meta.status, for example dato.home_page.meta.status

1 Like

Hi is there any update to this? would be nice to have the Publish and Unpublish separate role requirements to prevent non power users from un-publishing a singleton model.

Hey @felixh

You can solve the “do not allow unpublishing of singleton models”) with a tiny plugin that intercepts unpublish actions and checks the user’s role before letting them proceed. The hook you want is onBeforeItemsUnpublish, which fires any time the UI tries to unpublish one or more records, and you can block the action by returning false. The hook and behavior are documented here: https://www.datocms.com/docs/plugin-sdk/event-hooks#onBeforeItemsUnpublish and on the same page you’ll find ctx.currentRole, which exposes the role of the user triggering the action: https://www.datocms.com/docs/plugin-sdk/event-hooks. Singleton models are easy to detect because the Item Type has attributes.singleton === true, as shown in the CMA docs at https://www.datocms.com/docs/content-management-api/resources/item-type.

Here is a minimal example you can drop into a plugin. It prevents unpublishing if any of the selected records belong to a singleton model, unless the current role is explicitly allowed. You can tune the allowlist logic to match your project.

// src/index.ts
import { connect } from 'datocms-plugin-sdk';

connect({
  async onBeforeItemsUnpublish(items, ctx) {
    // Collect IDs of all singleton models in the project
    const singletonItemTypeIds = new Set(
      Object.values(ctx.itemTypes || {})
        .filter((it) => it?.attributes?.singleton)
        .map((it) => it!.id),
    );

    // Are we trying to unpublish at least one record from a singleton model?
    const touchesSingleton = items.some((item) => {
      const itemTypeId = item?.relationships?.item_type?.data?.id;
      return itemTypeId && singletonItemTypeIds.has(itemTypeId);
    });

    if (!touchesSingleton) return;

    // Simple role-based allowlist. Use role name here for readability,
    // or switch to role IDs if you prefer something immutable.
    const roleName = ctx.currentRole?.attributes?.name || 'Unknown';
    const allowedRoleNames = ['Administrator']; // adjust to your needs

    const isAllowed = allowedRoleNames.includes(roleName);
    if (isAllowed) return;

    // Block the action and inform the editor
    ctx.alert(
      'This record belongs to a singleton model. Unpublishing is not permitted for your role.',
    );
    return false;
  },
});

If you want to make the allowlist configurable by project admins instead of hardcoding role names, you can store an array of allowed role IDs or names in plugin settings and read them from ctx.plugin.attributes.parameters. The config screen docs are here if you want to wire that up: https://www.datocms.com/docs/plugin-sdk/config-screen.

That is all you need. The hook stops the unpublish action and shows an explanation, and ctx.currentRole gives you the control to let specific roles bypass the restriction. The official references that cover these pieces are the event hooks page at https://www.datocms.com/docs/plugin-sdk/event-hooks#onBeforeItemsUnpublish and the Item Type schema that exposes the singleton flag at https://www.datocms.com/docs/content-management-api/resources/item-type.

2 Likes

Ooooh, ill do a bit of investigating, but thanks for the write up. :smiley:

Hey m.finamore, one further question, What about scheduled un publishing. Would i be able to block that event aswell?

Hey @felixh, the short answer is that plugins cannot block scheduled unpublishing. onBeforeItemsUnpublish only fires for immediate unpublish actions triggered in the UI, while schedules are created in the UI but executed later by the backend, outside of the plugin runtime. You can see the available hooks here: https://www.datocms.com/docs/plugin-sdk/event-hooks.

If you need to enforce “never unpublish singletons” across both immediate and scheduled actions, the usual workaround is to cancel schedules as soon as they are created. When someone sets an unpublishing schedule, DatoCMS sends an update webhook and the record payload includes meta.unpublishing_scheduled_at. In your webhook handler you can check if the record’s model is a singleton and immediately call the Scheduled Unpublishing destroy endpoint for that item. Docs for scheduled unpublishing are here: create https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/create and delete https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/destroy. The item payload exposes meta.unpublishing_scheduled_at so it is easy to detect, see the record object here: https://www.datocms.com/docs/content-management-api/resources/item. Webhooks overview is here: https://www.datocms.com/docs/general-concepts/webhooks.

Here is a minimal example using our official client to auto‑cancel any scheduled unpublish on singleton models:

import express from 'express';
import { buildClient } from '@datocms/cma-client-node';

const client = buildClient({ apiToken: process.env.DATOCMS_API_TOKEN });

const app = express();
app.use(express.json());

app.post('/dato-webhook', async (req, res) => {
  try {
    const { event_type, entity_type, entity } = req.body;

    if (entity_type === 'item' && event_type === 'update') {
      const scheduledAt = entity?.meta?.unpublishing_scheduled_at;
      if (scheduledAt) {
        const itemTypeId = entity?.relationships?.item_type?.data?.id;
        const itemType = await client.itemTypes.find(itemTypeId); // https://www.datocms.com/docs/content-management-api/resources/item-type
        if (itemType.singleton) {
          await client.scheduledUnpublishing.destroy(entity.id); // https://www.datocms.com/docs/content-management-api/resources/scheduled-unpublishing/destroy
        }
      }
    }

    res.status(200).send('ok');
  } catch (e) {
    console.error(e);
    res.status(500).send('error');
  }
});

app.listen(3000);

If you prefer to prevent schedules at the source, remember scheduling uses the same “Publish/unpublish” permission used for immediate actions. You can restrict who is allowed to unpublish at the role level or via API tokens, but note that publish and unpublish are a single permission today. Roles and permissions are documented at https://www.datocms.com/docs/general-concepts/roles-and-permission-system, and there is an open feature request to split publish from unpublish here: https://community.datocms.com/t/separate-publish-unpublish-permission/5323.

So, combine the UI plugin you have for immediate actions with a small webhook like the one above to neutralize scheduled unpublishing as soon as it is created. That covers both paths.

Cool thanks. :smiley: