UID field does not automatically generate the slug from the attached field

System Information
  • Strapi Version: 5.0.0

Hi,
I’m using Strapi v5 and in a collection (products) and I’m trying to automatically generate the slug.
If I don’t set the field as required, the slug is not populated.
However, if I set it as required, when I create a new entry, it uses the name of the collection.
Since I want to make the field mandatory and non-editable, this becomes a major issue because it always generates a slug with the value ‘products’ instead of the attached-field.
Here is the schema of my collection:

{
  "kind": "collectionType",
  "collectionName": "products",
  "info": {
    "singularName": "product",
    "pluralName": "products",
    "displayName": "Products",
    "description": ""
  },
  "options": {
    "draftAndPublish": true
  },
  "pluginOptions": {
    "i18n": {
      "localized": true
    }
  },
  "attributes": {
    "seo": {
      "type": "component",
      "repeatable": false,
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "component": "base.seo"
    },
    "components": {
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "type": "dynamiczone",
      "components": [
        "composed.hero",
        "composed.banner-full-width-assistance",
        "composed.product-card"
      ]
    },
    "previewTitle": {
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "type": "string",
      "required": true
    },
    "previewAbstract": {
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "type": "text"
    },
    "previewImage": {
      "type": "media",
      "multiple": false,
      "required": true,
      "allowedTypes": [
        "images",
        "files"
      ],
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      }
    },
    "altPreviewImage": {
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "type": "string"
    },
    "slug": {
      "pluginOptions": {
        "i18n": {
          "localized": true
        }
      },
      "type": "uid",
      "targetField": "previewTitle",
      "required": true
    }
  }
}

Thanks in advance.

1 Like

Hello i’ve same problem; any news about?

1 Like

We solved with custom code inside js using strapi document middleware instead lifecycles.

Maybe in actual docs there is the need to clarify the usage of actual lifecycles.js in model.

This is our solutions
on src/index.ts

import { generateSlug, slugParamsPresent, slugAvailable } from './customSlugger';

const slugMe = {
  'api::product.product': 'previewTitle',
 }
 
 strapi.documents.use(async (context: any, next: any) => {

  if (!Object.keys(slugMe).includes(context.uid)) {
    return next();
  }

  if(['create', 'update'].includes(context.action)) {

    if (slugParamsPresent(context.params.data.slug) && await slugAvailable(context.params.data.slug, context)) {
      return next();
    }

    let field = context.params.data[slugMe[context.uid]]
    const newSlug = await generateSlug(field, context)
    context.params.data.slug = newSlug;
  }

  return next();
})

src/customSlugger.ts

 const generateSlug = async (slug: string, context: any) => {
  let generatedSlug = slugger(slug)
  let i = 1;
  let present = await slugAlreadyPresent(generatedSlug, context)

  while (present) {
    generatedSlug = slugger(`${slug} ${i}`);
    present = await slugAlreadyPresent(generatedSlug, context);
    i++;
  }

  return generatedSlug;
}

const slugger = (str: string) => {
  let sanitizedString = str.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
  sanitizedString = sanitizedString.replace(/[^a-zA-Z0-9 ]/g, '');
  return sanitizedString.replace(/\s/g, '-').toLowerCase();
}

const slugAlreadyPresent = async (slug: string, context: any) => {
  const elements = await strapi.documents(context.uid).findMany({
    locale: context.params.locale,
    filters: {
      slug: {
        $eq: slug
      }
    }
  });

  const filteredElements = elements.filter((element: any) => {
    return element.documentId != context.params.documentId
  })
  return filteredElements.length > 0;
}

const slugParamsPresent = (paramsSlug: any) => {
  return paramsSlug != undefined && paramsSlug != null && paramsSlug != ''
}

const slugAvailable = async (slug: string, context: any) => {
  return await slugAlreadyPresent(slug, context) == false;
}

export {
  generateSlug,
  slugParamsPresent,
  slugAvailable
}
1 Like

Hello,

You can use Strapi functions to generate UID like this :

export default factories.createCoreController('api::municipality.municipality', ({ strapi }) => ({
    async create(ctx) {
        // Generate a unique slug for the new municipality
        // The slug will be based on the municipality name and ensure URL-friendliness
        const slug = await strapi.plugins[
            "content-manager"
        ].services.uid.generateUIDField({
            contentTypeUID: "api::municipality.municipality",
            field: "slug",
            data: ctx.request.body.data,
        });

        // Attach the generated slug to the request data
        ctx.request.body.data.slug = slug;

        // Proceed with the standard creation process
        const response = await super.create(ctx);
        return response;
    },
    async update(ctx) {
        // Regenerate the slug when updating municipality details
        // This maintains consistency between the name and slug
        const slug = await strapi.plugins[
            "content-manager"
        ].services.uid.generateUIDField({
            contentTypeUID: "api::municipality.municipality",
            field: "slug",
            data: ctx.request.body.data,
        });
        ctx.request.body.data.slug = slug;
        const response = await super.update(ctx);
        return response;
    }
}));