Slug
Generate unique Slugs (aka; keys / url segments) based on the item's data.
Usage
By default, slugs are generated from a name or title field (in that order)
if they exist. The field can be specified explicitly with the from option
(from: 'username'), or for more advanced use-cases, a generate function
can be provided (generate: ({ resolvedData, existingItem }) => mySlugFunc(resolvedData.username))
See the example below for the result of an example mutation.
Using the defaults
As we have specified a title field, its value will be used to generate a
unique slug.
const { Slug, Text } = require('@keystonejs/fields');
const { Keystone } = require('@keystonejs/keystone');
const keystone = new Keystone(/* ... */);
keystone.createList('Post', {
fields: {
title: { type: Text },
url: { type: Slug },
},
});
Specifying a field
The item's username value will be used to generate a unique slug.
const { Slug, Text } = require('@keystonejs/fields');
const { Keystone } = require('@keystonejs/keystone');
const keystone = new Keystone(/* ... */);
keystone.createList('User', {
fields: {
username: { type: Text },
url: {
type: Slug,
from: 'username',
},
},
});
Custom generate method
const { Slug, Text, DateTime } = require('@keystonejs/fields');
const { Keystone } = require('@keystonejs/keystone');
const slugify = require('slugify');
const keystone = new Keystone(/* ... */);
keystone.createList('Post', {
fields: {
title: { type: Text },
postedAt: { type: DateTime },
url: {
type: Slug,
generate: ({ resolvedData }) => slugify(resolvedData.title + '-' + resolvedData.postedAt),
},
},
});
Slug stability
The Slug field attempts to reuse the same value across updates (ie;
"stability"). This is particularly important when slugs are used for URL
segments to ensure URLs don't change.
For example, if you create an item with a slug abc123, then perform an
update mutation without changing any of the data which the slug initial
generation was based on, the slug should stay as abc123.
Caveats with updates and makeUnique
There is one situation where the Slug field cannot guarantee stability; when:
- The
regenerateOnUpdateflag istrue, and - Performing an
updatemutation, and - A
slugvalue is passed in which is not unique in the list
For example:
Perform a
createmutation:createPost(data: { slug: "hello-world" }) { slug }.- Result:
{ slug: "hello-world" }
- Result:
Perform a second
createmutation with the same slug:createPost(data: { slug: "hello-world" }) { id slug }.- Result (approximately):
{ id: "1", slug: "hello-world-weer84fs" }
- Result (approximately):
Perform an update to the second item, with the same slug as the first (again):
updatePost(id: "1", data: { slug: "hello-world" }) { id slug }.- Result (approximately):
{ id: "1", slug: "hello-world-uyi3lh32" } - The slug has changed, even though we passed the same slug in. This happens
because there is no way to know what the previously passed-in slug was, only
the most recently uniquified slug (ie;
"hello-world-weer84fs").
- Result (approximately):
Workarounds
- Don't pass in values for the
Slugfield. Instead, create ageneratefunction which does the work for you in a deterministic way. In this scenario, we are able to compare what would have previously been generated as the slug to the newly generated slug and re-use the old one if they match. - Specify a deterministic
makeUniquefunction (the default is to add a random suffix). This will ensure that when the duplicate slug is detected, it will re-generate the same "uniqueified" slug each time. This can be done by adding an incrementing number on each call.
Example
Given the following list config:
keystone.createList('Post', {
fields: {
title: { type: Text },
url: {
type: Slug,
from: 'title',
},
},
});
A mutation to create a new item will auto-generate a slug:
mutation {
createPost(data: { title: "Why I ♥ KeystoneJS" }) {
id
title
url
}
}
# Result:
# {
# createPost: {
# id: "1",
# title: "Why I ♥ KeystoneJS",
# url: "why-i-love-keystonejs"
# }
# }
Because we've left isUnique at its default value (isUnique: true), a
subsequently created item with the same title will generate a unique slug:
mutation {
createPost(data: { title: "Why I ♥ KeystoneJS" }) {
id
title
url
}
}
# Result:
# {
# createPost: {
# id: "2",
# title: "Why I ♥ KeystoneJS",
# url: "why-i-love-keystonejs-2108fh3"
# }
# }
You can also manually override the slug's value:
mutation {
createPost(data: { title: "Why I ♥ KeystoneJS", url: "keystonejs-is-great" }) {
id
title
url
}
}
# Result:
# {
# createPost: {
# id: "2",
# title: "Why I ♥ KeystoneJS",
# url: "keystonejs-is-great"
# }
# }
And overwritten slugs will be uniquified for you when isUnique: true (with
one caveat):
mutation {
createPost(data: { title: "Why I ♥ KeystoneJS", url: "keystonejs-is-great" }) {
id
title
url
}
}
# Result:
# {
# createPost: {
# id: "2",
# title: "Why I ♥ KeystoneJS",
# url: "keystonejs-is-great-f80p5sm"
# }
# }
Config
| Option | Type | Default | Description |
|---|---|---|---|
from | String | undefined | Specify a field whos value will be used to generate a slug. An error will be thrown if the value of the specified field is not of type string. NOTE: Only one of the from or generate options should be set. |
generate | Function<String> | Either name, title, or the first non-id field found | Will be passed { resolvedData, existingItem } as the first parameter from which you can generate a slug. An error will be thrown if the returned value is not of type string. NOTE: Only one of the from or generate options should be set. |
makeUnique | Function<String> | Appends a hyphen followed by 7-10 random lowercase alpha-num characters. | If isUnique === true and the slug returned from generate is not unique, this method will be executed. This method receives a single parameter { value, slug, previousSlug, generatedSlug } where slug is the slug to make unique, previousSlug is the previous result of calling makeUnique, and generatedSlug is the original result of calling generate / any value passed in the mutation. If the uniqueified string returned is still not unique, this function will be called again with the uniqueified value set to previousSlug. |
alwaysMakeUnique | Boolean | false | Always run the makeUnique method, even if the uniquified slug from generate does not collide with any existing slugs. The default of only uniquifying on collision can lead to subtle data leak vulnerabilities such as revealing the existence of a slug for a given field value to exist, even if that item in the list is otherwise innaccessible to queries (eg via Access Control). For example; a deactivated user with slug sam-smith will cause a collision for new accounts with a name of Sam Smith, revealing the existence of previously created accounts of that name by checking if the returned slug has a unique string appended. |
regenerateOnUpdate | Boolean | true | If no value is received during an update<List> mutation, generate a value as per a create<List> mutation. NOTE: If a value is received, it will overwrite this setting. |
isUnique | Boolean | true | Ensures makeUnique is executed if the value is not unique. Adds a unique database index that allows only unique values to be stored. Implies isIndexed. |
isIndexed | Boolean | true | Set a database index on this field. Setting isUnique will also set isIndexed to true. |
GraphQL
Slug fields use the String type in GraphQL.
Input fields
| Field name | Type | Description |
|---|---|---|
${path} | String | A slug, or blank to execute generate |
Output fields
| Field name | Type | Description |
|---|---|---|
${path} | String | A unique slug |
Filters
| Field name | Type | Description |
|---|---|---|
${path} | String | Exact match to the String provided |
${path}_contains | String | Contains the String provided as a substring |
${path}_starts_with | String | Starts with the String provided |
${path}_ends_with | String | Ends with the String provided |
${path}_in | [String] | In the array of Strings provided |
${path}_not | String | Not an exact match to the String provided |
${path}_not_contains | String | Does not contain the String provided as a substring |
${path}_not_starts_with | String | Does not start with the String provided |
${path}_not_ends_with | String | Does not end with the String provided |
${path}_not_in | [String] | Not in the array of Strings provided |
${path}_i | String | Case insensitive version of ${path} |
${path}_not_i | String | Case insensitive version of ${path}_not |
${path}_contains_i | String | Case insensitive version of ${path}_contains |
${path}_not_contains_i | String | Case insensitive version of ${path}_not_contains |
${path}_starts_with_i | String | Case insensitive version of ${path}_starts_with |
${path}_not_starts_with_i | String | Case insensitive version of ${path}_not_starts_with |
${path}_ends_with_i | String | Case insensitive version of ${path}_ends_with |
${path}_not_ends_with_i | String | Case insensitive version of ${path}_not_ends_with |