Skip to content
This repository was archived by the owner on Sep 16, 2025. It is now read-only.

Commit 2d53829

Browse files
fix(slugifyWithCount): persist slug count (#82)
* fix(slugification): persist slug count * fix(slugification): ensure correct count used for old plugin versions and projects with existing slugs * fix(slug CT): do not show plugin CT in content manager * fix(syncSlugCount): ensure we have slugs to sync for createMany * refactor(buildSlug): use early return
1 parent 9c336eb commit 2d53829

File tree

8 files changed

+167
-26
lines changed

8 files changed

+167
-26
lines changed

server/bootstrap/index.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22

33
const { buildSettings } = require('./buildSettings');
44
const { setupLifecycles } = require('./setupLifecycles');
5+
const { syncSlugCount } = require('./syncSlugCount');
6+
7+
module.exports = async () => {
8+
const settings = await buildSettings();
9+
10+
if (settings.slugifyWithCount) {
11+
// Ensure correct count used for old plugin versions and projects with existing slugs.
12+
await syncSlugCount(settings);
13+
}
514

6-
module.exports = async ({ strapi }) => {
7-
const settings = await buildSettings(strapi);
815
setupLifecycles(settings);
916
};

server/bootstrap/syncSlugCount.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
const syncSlugCount = async (settings) => {
4+
const entries = await strapi.entityService.findMany('plugin::slugify.slug', {
5+
filters: { createdAt: { $gt: 1 } },
6+
});
7+
8+
// if entries aready present we can skip sync
9+
if (entries && entries.length) {
10+
return;
11+
}
12+
13+
strapi.log.info('[slugify] syncing slug count for registered content types');
14+
15+
const slugs = new Map();
16+
17+
// chec slugs in each reigistered model
18+
for (const uid in settings.modelsByUID) {
19+
if (!Object.hasOwnProperty.call(settings.modelsByUID, uid)) {
20+
continue;
21+
}
22+
23+
const model = settings.modelsByUID[uid];
24+
25+
// using db query to avoid the need to check if CT has draftAndPublish enabled
26+
const modelEntries = await strapi.db.query(model.uid).findMany({
27+
filters: { createdAt: { $gt: 1 } },
28+
});
29+
30+
strapi.log.info(`[slugify] syncing slug count for ${model.uid}`);
31+
for (const entry of modelEntries) {
32+
const slug = entry[model.field];
33+
if (!slug) {
34+
continue;
35+
}
36+
37+
const record = slugs.get(getNonAppendedSlug(slug));
38+
if (!record) {
39+
slugs.set(slug, { slug, count: 1 });
40+
continue;
41+
}
42+
43+
slugs.set(record.slug, { slug: record.slug, count: record.count + 1 });
44+
}
45+
strapi.log.info(`[slugify] sync for ${model.uid} completed`);
46+
}
47+
48+
if (slugs.size) {
49+
// create all required records
50+
const createResponse = await strapi.db.query('plugin::slugify.slug').createMany({
51+
data: [...slugs.values()],
52+
});
53+
54+
strapi.log.info(
55+
`[slugify] ${createResponse.count} out of ${slugs.size} slugs synced successfully`
56+
);
57+
} else {
58+
strapi.log.info('[slugify] No syncable slugs found');
59+
}
60+
};
61+
62+
// removes any appended number from a slug/string if found
63+
const getNonAppendedSlug = (slug) => {
64+
const match = slug.match('[\\-]{1}[\\d]+$');
65+
66+
if (!match) {
67+
return slug;
68+
}
69+
70+
return slug.replace(match[0], '');
71+
};
72+
73+
module.exports = {
74+
syncSlugCount,
75+
};

server/content-types/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
3+
const slugSchema = require('./slug/schema.json');
4+
5+
module.exports = {
6+
slug: {
7+
schema: slugSchema,
8+
},
9+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"kind": "collectionType",
3+
"collectionName": "slugs",
4+
"info": {
5+
"singularName": "slug",
6+
"pluralName": "slugs",
7+
"displayName": "slug"
8+
},
9+
"options": {
10+
"draftAndPublish": false,
11+
"comment": ""
12+
},
13+
"pluginOptions": {
14+
"content-manager": {
15+
"visible": false
16+
},
17+
"content-type-builder": {
18+
"visible": false
19+
}
20+
},
21+
"attributes": {
22+
"slug": {
23+
"type": "text"
24+
},
25+
"count": {
26+
"type": "integer"
27+
}
28+
}
29+
}

server/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const bootstrap = require('./bootstrap');
44
const config = require('./config');
5+
const contentTypes = require('./content-types');
56
const controllers = require('./controllers');
67
const register = require('./register');
78
const routes = require('./routes');
@@ -10,6 +11,7 @@ const services = require('./services');
1011
module.exports = {
1112
bootstrap,
1213
config,
14+
contentTypes,
1315
controllers,
1416
register,
1517
routes,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const slugify = require('@sindresorhus/slugify');
2+
3+
const buildSlug = async (string, settings) => {
4+
let slug = slugify(string, settings.slugifyOptions);
5+
6+
// slugify with count
7+
if (!settings.slugifyWithCount) {
8+
return slug;
9+
}
10+
11+
const slugEntry = await strapi.db.query('plugin::slugify.slug').findOne({
12+
select: ['id', 'count'],
13+
where: { slug },
14+
});
15+
16+
// if no result then count is 1 and base slug is returned
17+
if (!slugEntry) {
18+
await strapi.entityService.create('plugin::slugify.slug', {
19+
data: {
20+
slug,
21+
count: 1,
22+
},
23+
});
24+
25+
return slug;
26+
}
27+
28+
const count = slugEntry.count + 1;
29+
await strapi.entityService.update('plugin::slugify.slug', slugEntry.id, {
30+
data: {
31+
count,
32+
},
33+
});
34+
35+
return `${slug}-${count}`;
36+
};
37+
38+
module.exports = {
39+
buildSlug,
40+
};

server/services/slug-service/index.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
'use strict';
22

33
const _ = require('lodash');
4-
const { toSlug } = require('../../utils/slugification');
54
const { getPluginService } = require('../../utils/getPluginService');
65
const { shouldUpdateSlug } = require('./shoudUpdateSlug');
76
const { getReferenceFieldValues } = require('./getReferenceFieldValues');
7+
const { buildSlug } = require('./buildSlug');
88

99
module.exports = ({ strapi }) => ({
1010
async slugify(ctx) {
@@ -40,13 +40,10 @@ module.exports = ({ strapi }) => ({
4040
}
4141

4242
referenceFieldValues = referenceFieldValues.join(' ');
43-
const toSlugOptions = settings.slugifyOptions;
44-
if (settings.slugifyWithCount) {
45-
toSlugOptions.slugifyWithCount = settings.slugifyWithCount;
46-
}
4743

4844
// update slug field based on action type
49-
const slug = toSlug(referenceFieldValues, toSlugOptions);
45+
const slug = await buildSlug(referenceFieldValues, settings);
46+
5047
if (ctx.action === 'beforeCreate' || ctx.action === 'beforeUpdate') {
5148
data[field] = slug;
5249
} else if (ctx.action === 'afterCreate') {

server/utils/slugification.js

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)