slugifiable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +178 -0
- data/Rakefile +4 -0
- data/lib/slugifiable/model.rb +159 -0
- data/lib/slugifiable/version.rb +5 -0
- data/lib/slugifiable.rb +9 -0
- data/sig/slugifiable.rbs +4 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d06633ba0907f8ee400648856e3d8dbc95fc647be40d60780ae01ebbb6eeb7d3
|
4
|
+
data.tar.gz: d40cae2d3fc081af04bd6fac08ea8640d8174c1e59b07652c92bab72eb8a18ca
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 39dfad3e03b4cfef9946a929c5fe21585f2aab1ef1922d2f13ad8d10d6d4b732e3eaf6d4dcff545d9664d0395909fa6fa5e40a92801627625ef4034467b93bd9
|
7
|
+
data.tar.gz: 8b726b22d56141b633f415460f8a8f8e7042379bc06aebb383f15360914b294bf29a0fffe2aa5e97e25577dab3e962d2b221bf7f18ab0d8822437f77ca97bfef
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Javi R
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# 🐌 `slugifiable` - Rails gem to generate SEO-friendly slugs
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/slugifiable)
|
4
|
+
|
5
|
+
Automatically generates unique slugs for your Rails' model records, so you can expose SEO-friendly URLs.
|
6
|
+
|
7
|
+
Example:
|
8
|
+
```
|
9
|
+
https://myapp.com/products/big-red-backpack-321678
|
10
|
+
```
|
11
|
+
|
12
|
+
Where `big-red-backpack-321678` is the slug.
|
13
|
+
|
14
|
+
`slugifiable` can generate:
|
15
|
+
- Slugs like `"big-red-backpack"` or `"big-red-backpack-321678"`: unique, string-based slugs based on any attribute, such as `product.name`
|
16
|
+
- Slugs like `"d4735e3a265"`: unique **hex string slugs**
|
17
|
+
- Slugs like `321678`: unique **number-only slugs**
|
18
|
+
|
19
|
+
## Why
|
20
|
+
|
21
|
+
When building Rails apps, we usually need to expose _something_ in the URL to identify a record, like:
|
22
|
+
```
|
23
|
+
https://myapp.com/products/123
|
24
|
+
```
|
25
|
+
|
26
|
+
But exposing IDs (like `123`) is not usually good practice. It's not SEO-friendly, it can give away how many records you have in the database, it could also be an attack vector, and it just feels off.
|
27
|
+
|
28
|
+
It would be much better to have a random-like string or number instead, while still remaining unique and identifiable:
|
29
|
+
```
|
30
|
+
https://myapp.com/products/d4735e3a265
|
31
|
+
```
|
32
|
+
|
33
|
+
Or better yet, use other instance attribute (like `product.name`) to build the slug:
|
34
|
+
```
|
35
|
+
https://myapp.com/products/big-red-backpack
|
36
|
+
```
|
37
|
+
|
38
|
+
`slugifiable` takes care of building all these kinds of slugs for you.
|
39
|
+
|
40
|
+
## Installation
|
41
|
+
|
42
|
+
Add this line to your application's Gemfile:
|
43
|
+
```ruby
|
44
|
+
gem 'slugifiable'
|
45
|
+
```
|
46
|
+
|
47
|
+
Then run `bundle install`.
|
48
|
+
|
49
|
+
After installing the gem, add `include Slugifiable::Model` to any ActiveRecord model, like this:
|
50
|
+
```ruby
|
51
|
+
class Product < ApplicationRecord
|
52
|
+
include Slugifiable::Model # Adding this provides all the required slug-related methods to your model
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
That's it!
|
57
|
+
|
58
|
+
Then you can, for example, get the slug for a product like this:
|
59
|
+
```ruby
|
60
|
+
Product.first.slug
|
61
|
+
=> "4e07408562b"
|
62
|
+
```
|
63
|
+
|
64
|
+
You can also define how to generate slugs:
|
65
|
+
```ruby
|
66
|
+
class Product < ApplicationRecord
|
67
|
+
include Slugifiable::Model
|
68
|
+
generate_slug_based_on :name
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
And this will generate slugs based on your `Product` instance `name`, like:
|
73
|
+
```ruby
|
74
|
+
Product.first.slug
|
75
|
+
=> "big-red-backpack"
|
76
|
+
```
|
77
|
+
|
78
|
+
If your model has a `slug` attribute in the database, `slugifiable` will automatically generate a slug for that model upon instance creation, and save it to the DB.
|
79
|
+
|
80
|
+
If you're generating slugs based off the model `id`, you can also set a desired length:
|
81
|
+
```ruby
|
82
|
+
class Product < ApplicationRecord
|
83
|
+
include Slugifiable::Model
|
84
|
+
generate_slug_based_on :id, length: 6
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
Which would return something like:
|
89
|
+
```ruby
|
90
|
+
Product.first.slug
|
91
|
+
=> "6b86b2"
|
92
|
+
```
|
93
|
+
|
94
|
+
More details in the "How to use" section.
|
95
|
+
|
96
|
+
## How to use
|
97
|
+
|
98
|
+
Slugs should never change, so it's recommended you save your slugs to the database.
|
99
|
+
|
100
|
+
Therefore, all models that include `Slugifiable::Model` should have a `slug` attribute that persists the slug in the database. If your model doesn't have a `slug` attribute yet, just run:
|
101
|
+
```
|
102
|
+
rails g migration addSlugTo<MODEL_NAME> slug:text
|
103
|
+
```
|
104
|
+
|
105
|
+
where `<MODEL_NAME>` is your model name in plural, and then run:
|
106
|
+
```
|
107
|
+
rails db:migrate
|
108
|
+
```
|
109
|
+
|
110
|
+
And your model should now have a `slug` attribute in the database.
|
111
|
+
|
112
|
+
When a model has a `slug` attribute, `slugifiable` automatically generates a slug for that model upon instance creation, and saves it to the DB.
|
113
|
+
|
114
|
+
`slugifiable` can also work without persisting slugs to the databse, though: you can always run `.slug`, and that will give you a valid, unique slug for your record.
|
115
|
+
|
116
|
+
### Define how slugs are generated
|
117
|
+
|
118
|
+
By default, when you include `Slugifiable::Model`, slugs will be generated as a random-looking string based off the record `id` (SHA hash)
|
119
|
+
|
120
|
+
`slugifiable` supports both `id` and `uuid`.
|
121
|
+
|
122
|
+
The default setting is:
|
123
|
+
```ruby
|
124
|
+
generate_slug_based_on id: :hex_string
|
125
|
+
```
|
126
|
+
|
127
|
+
Which returns slugs like: `d4735e3a265`
|
128
|
+
|
129
|
+
If you don't like hex strings, you can get number-only slugs with:
|
130
|
+
```ruby
|
131
|
+
generate_slug_based_on id: :number
|
132
|
+
```
|
133
|
+
|
134
|
+
Which will return slugs like: `321678` – nonconsecutive, nonincremental, not a total count.
|
135
|
+
|
136
|
+
When you're generating obfuscated slugs (based on `id`), you can specify a desired slug length:
|
137
|
+
```ruby
|
138
|
+
generate_slug_based_on id: :number, length: 3
|
139
|
+
```
|
140
|
+
|
141
|
+
The length should be a positive number between 1 and 64.
|
142
|
+
|
143
|
+
If instead of obfuscated slugs you want human-readable slugs, you can specify an attribute to base your slugs off of. For example:
|
144
|
+
```ruby
|
145
|
+
generate_slug_based_on :name
|
146
|
+
```
|
147
|
+
|
148
|
+
Will look for a `name` attribute in your instance, and use its value to generate the slug. So if you have a product like:
|
149
|
+
```ruby
|
150
|
+
Product.first.name
|
151
|
+
=> "Big Red Backpack"
|
152
|
+
```
|
153
|
+
|
154
|
+
then the slug will be computed as:
|
155
|
+
```ruby
|
156
|
+
Product.first.slug
|
157
|
+
=> "big-red-backpack"
|
158
|
+
```
|
159
|
+
|
160
|
+
There may be collisions if two records share the same name – but slugs should be unique! To resolve this, when this happens, `slugifiable` will append a unique string at the end to make the slug unique:
|
161
|
+
```ruby
|
162
|
+
Product.first.slug
|
163
|
+
=> "big-red-backpack-321678"
|
164
|
+
```
|
165
|
+
|
166
|
+
## Development
|
167
|
+
|
168
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
169
|
+
|
170
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
171
|
+
|
172
|
+
## Contributing
|
173
|
+
|
174
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/slugifiable. Our code of conduct is: just be nice and make your mom proud of what you do and post online.
|
175
|
+
|
176
|
+
## License
|
177
|
+
|
178
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
module Slugifiable
|
2
|
+
module Model
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# This concern makes objects have a string slug based on their ID or another specified attribute
|
6
|
+
#
|
7
|
+
# To use, include this in any ActiveRecord model:
|
8
|
+
# ```
|
9
|
+
# include Slugifiable::Model
|
10
|
+
# ```
|
11
|
+
#
|
12
|
+
# By default all slugs will be a string computed from the record ID:
|
13
|
+
# ```
|
14
|
+
# generate_slug_based_on :id
|
15
|
+
# ```
|
16
|
+
#
|
17
|
+
# but optionally, you can also specify to compute the slug as a number:
|
18
|
+
# ```
|
19
|
+
# generate_slug_based_on id: :number
|
20
|
+
# ```
|
21
|
+
#
|
22
|
+
# or compute the slug based off any other attribute:
|
23
|
+
# ```
|
24
|
+
# generate_slug_based_on :name
|
25
|
+
# ```
|
26
|
+
|
27
|
+
DEFAULT_SLUG_GENERATION_STRATEGY = :compute_slug_as_string
|
28
|
+
DEFAULT_SLUG_STRING_LENGTH = 11
|
29
|
+
DEFAULT_SLUG_NUMBER_LENGTH = 6
|
30
|
+
|
31
|
+
included do
|
32
|
+
after_create :set_slug
|
33
|
+
after_find :update_slug_if_nil
|
34
|
+
validates :slug, uniqueness: true
|
35
|
+
end
|
36
|
+
|
37
|
+
class_methods do
|
38
|
+
def generate_slug_based_on(strategy, options = {})
|
39
|
+
define_method(:generate_slug_based_on) do
|
40
|
+
[strategy, options]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing(missing_method, *args, &block)
|
47
|
+
if missing_method.to_s == "slug" && !self.methods.include?(:slug)
|
48
|
+
compute_slug
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def compute_slug
|
55
|
+
strategy, options = determine_slug_generation_method
|
56
|
+
|
57
|
+
length = options[:length] if options.is_a?(Hash) || nil
|
58
|
+
|
59
|
+
if strategy == :compute_slug_based_on_attribute
|
60
|
+
self.send(strategy, options)
|
61
|
+
else
|
62
|
+
self.send(strategy, length)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def compute_slug_as_string(length = DEFAULT_SLUG_STRING_LENGTH)
|
67
|
+
length ||= DEFAULT_SLUG_STRING_LENGTH
|
68
|
+
(Digest::SHA2.hexdigest self.id.to_s).first(length)
|
69
|
+
end
|
70
|
+
|
71
|
+
def compute_slug_as_number(length = DEFAULT_SLUG_NUMBER_LENGTH)
|
72
|
+
length ||= DEFAULT_SLUG_NUMBER_LENGTH
|
73
|
+
generate_random_number_based_on_id_hex(length)
|
74
|
+
end
|
75
|
+
|
76
|
+
def compute_slug_based_on_attribute(attribute_name)
|
77
|
+
return compute_slug_as_string unless self.attributes.include?(attribute_name.to_s)
|
78
|
+
|
79
|
+
base_slug = self.send(attribute_name)&.to_s&.strip&.parameterize
|
80
|
+
base_slug = base_slug.presence || generate_random_number_based_on_id_hex
|
81
|
+
|
82
|
+
unique_slug = generate_unique_slug(base_slug)
|
83
|
+
unique_slug.presence || generate_random_number_based_on_id_hex
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def generate_random_number_based_on_id_hex(length = DEFAULT_SLUG_NUMBER_LENGTH)
|
89
|
+
length ||= DEFAULT_SLUG_NUMBER_LENGTH
|
90
|
+
((Digest::SHA2.hexdigest(id.to_s)).hex % (10 ** length))
|
91
|
+
end
|
92
|
+
|
93
|
+
def generate_unique_slug(base_slug)
|
94
|
+
slug_candidate = base_slug
|
95
|
+
|
96
|
+
return slug_candidate unless slug_persisted?
|
97
|
+
|
98
|
+
# Collision resolution logic:
|
99
|
+
|
100
|
+
count = 0
|
101
|
+
|
102
|
+
while self.class.exists?(slug: slug_candidate)
|
103
|
+
count += 1
|
104
|
+
# slug_candidate = "#{base_slug}-#{count}"
|
105
|
+
slug_candidate = "#{base_slug}-#{compute_slug_as_number}"
|
106
|
+
end
|
107
|
+
|
108
|
+
slug_candidate
|
109
|
+
end
|
110
|
+
|
111
|
+
def determine_slug_generation_method
|
112
|
+
return [DEFAULT_SLUG_GENERATION_STRATEGY, {}] unless respond_to?(:generate_slug_based_on)
|
113
|
+
|
114
|
+
strategy, options = generate_slug_based_on
|
115
|
+
options.merge!(strategy) if strategy.is_a? Hash
|
116
|
+
|
117
|
+
if strategy.is_a?(Symbol)
|
118
|
+
if strategy == :id
|
119
|
+
return [:compute_slug_as_string, options]
|
120
|
+
else
|
121
|
+
return [:compute_slug_based_on_attribute, strategy]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if strategy.is_a?(Hash)
|
126
|
+
if strategy.key?(:id)
|
127
|
+
case strategy[:id]
|
128
|
+
when :hex_string
|
129
|
+
return [:compute_slug_as_string, options]
|
130
|
+
when :number
|
131
|
+
return [:compute_slug_as_number, options]
|
132
|
+
else
|
133
|
+
return [DEFAULT_SLUG_GENERATION_STRATEGY, options]
|
134
|
+
end
|
135
|
+
elsif strategy.key?(:attribute)
|
136
|
+
return [:compute_slug_based_on_attribute, strategy[:attribute], options]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
[DEFAULT_SLUG_GENERATION_STRATEGY, options]
|
141
|
+
end
|
142
|
+
|
143
|
+
def slug_persisted?
|
144
|
+
self.methods.include?(:slug) && self.attributes.include?("slug")
|
145
|
+
end
|
146
|
+
|
147
|
+
def set_slug
|
148
|
+
return unless slug_persisted?
|
149
|
+
|
150
|
+
self.slug = compute_slug if id_changed? || slug.blank?
|
151
|
+
self.save
|
152
|
+
end
|
153
|
+
|
154
|
+
def update_slug_if_nil
|
155
|
+
set_slug if slug_persisted? && self.slug.nil?
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
data/lib/slugifiable.rb
ADDED
data/sig/slugifiable.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: slugifiable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- rameerez
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 6.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 6.0.0
|
27
|
+
description: Automatically generates unique and SEO-friendly slugs for your Rails
|
28
|
+
ActiveRecord model records, so you can expose them in URLs. Allows for string and
|
29
|
+
number-based slugs.
|
30
|
+
email:
|
31
|
+
- rubygems@rameerez.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- CHANGELOG.md
|
37
|
+
- LICENSE.txt
|
38
|
+
- README.md
|
39
|
+
- Rakefile
|
40
|
+
- lib/slugifiable.rb
|
41
|
+
- lib/slugifiable/model.rb
|
42
|
+
- lib/slugifiable/version.rb
|
43
|
+
- sig/slugifiable.rbs
|
44
|
+
homepage: https://github.com/rameerez/slugifiable
|
45
|
+
licenses:
|
46
|
+
- MIT
|
47
|
+
metadata:
|
48
|
+
allowed_push_host: https://rubygems.org
|
49
|
+
homepage_uri: https://github.com/rameerez/slugifiable
|
50
|
+
source_code_uri: https://github.com/rameerez/slugifiable
|
51
|
+
changelog_uri: https://github.com/rameerez/slugifiable/blob/main/CHANGELOG.md
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 3.0.0
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
requirements: []
|
67
|
+
rubygems_version: 3.5.16
|
68
|
+
signing_key:
|
69
|
+
specification_version: 4
|
70
|
+
summary: Easily generate unique and SEO optimized slugs for your Rails ActiveRecord
|
71
|
+
model records, based on any attribute
|
72
|
+
test_files: []
|