Gem 探報録 Shortener: Rails側で短縮URLを生成しよう！

# 【概要】

~~初めて自分主導で Gem を導入するため、ざっと調べてみた。~~

# 【本文】
## □ shortner とは？
- Rails アプリケーション内で短縮URLを生成できる！
- 短縮 URL を利用した計測もできる！（ユーザー等のモデル毎の計測も！）
- めっちゃ長いパラメータ付きのURLも省略できる！
- アプリケーション内でも外部でも、どこでもリダイレクトするよ！

```ruby:短縮URLとは
# 長いURLが...
"http://localhost:3000/account_activations/C7EYgDf43N5OOh0S92wJlg/edit?email=user%40example.com"

# 短いURLに...!?
"http://localhost:3000/rd/fisafaf"
```

### ■ 概要
- Shortener は 大別して３つの要素で構成される
  - Model では、短縮 URL の情報を保存
  - Controller では、短縮 URL のアクセスが来たら、元の URL へリダイレクト
  - Helper では、controllers と views で短縮 URL を生成
- Shortener のイカす所
  - 301 でリダイレクトさせる（ページランクに影響するそうな）。[^1]
  - 短縮 URL の英数字は ユニークである。
  - 短縮前 URL の使用回数を記録する。
  - リンクは、ユーザに関連付けが可能で、特定ユーザとリンク情報との統計情報が取得できる。

[^1]: [参考: 301リダイレクト \- Search Console Community](https://support.google.com/webmasters/forum/AAAAzAMeFTExq6lhPUt_V8/?hl=hu)

---
- 開発環境
    - Ruby: 2.4.9
    - Rails: 5.1.2
- 参考: 学習情報 URL
    - [jpmcgrath/shortener](https://github.com/jpmcgrath/shortener/)

---

### ■ 初期設定
**○ 1： Gemfile に追加**

```Gemfile
gem 'shortener'
```

```bash:bash
bundle install
```


**○ 2： generator を実行して短縮URL用のテーブル生成**[^2][^3]

[^2]: [参考: MySQL5\.6でActiveRecordのencodingがutf8mb4だとKey長すぎ問題の対応 \- もちゅろぐ](https://blog.mothule.com/ruby/rails/active-record/ruby-rails-activerecord-fix-mysql56-encode-utf8mb4-key-too-length)

[^3]: [参考: 第32回　InnoDBインデックスの最大キー長について：MySQL道普請便り｜gihyo\.jp … 技術評論社](https://gihyo.jp/dev/serial/01/mysql-road-construction-news/0032)



```bash:bash
rails generate shortener
〜 migratation ファイルを以下の通り変更する〜
rails db:migrate
```

```bash:失敗した場合のlog
# MySQL におけるデフォルトのキー長は、 767 bytes であるため、生成ファイルだとエラーが発生する。
StandardError: An error has occurred, all later migrations canceled:

Mysql2::Error: Specified key was too long; max key length is 3072 bytes: CREATE  INDEX `index_shortened_urls_on_url`  ON `shortened_urls` (`url`(2083))
```

```diff:~timestamp~_create_shortened_urls_table.rb
# MySQLの設定によりキー長を 3072bytes まで拡張できるが、デフォルト値を使用する。
...
-  t.text :url, null: false, length: 2083
+  t.text :url, null: false, length: 767
...
-  add_index :shortened_urls, :url, length: 2083
+  add_index :shortened_urls, :url, length: 767
...
```

**○ 3： ルーティング設定**

```ruby:routes.rb
...
+ get 'rd/:id', to: "shortener/shortened_urls#show", as: "shortend"
...
```

**○ 4： 短縮URLを生成**

```ruby:bash
Shortener::ShortenedUrl.generate("https://www.tbs.co.jp/anime/machikado/")
=> #<Shortener::ShortenedUrl:0x00005593275cf228
 id: 1,
 owner_id: nil,
 owner_type: nil,
 url: "https://comic-fuz.com/series/1583",
 unique_key: "d91z2",
 category: nil,
 use_count: 0,
 expires_at: nil,
 created_at: Sun, 08 Mar 2020 04:31:43 UTC +00:00,
 updated_at: Sun, 08 Mar 2020 04:31:43 UTC +00:00>

token = Shortener::ShortenedUrl.find(1).unique_key
=> "d91z2"

Rails.application.routes.url_for(host: "localhost:3000", controller: "shortener/shortened_urls", action: :show, id: token)
=> "http://localhost:3000/rd/d91z2"
```


**○ 5： 短縮URLにアクセスしてみると...**

```ruby:bash
# `use_count`の通りアクセス数がカウントされる
Shortener::ShortenedUrl.find(1)
=> #<Shortener::ShortenedUrl:0x0000562cdbe787c8
 id: 1,
 owner_id: nil,
 owner_type: nil,
 url: "https://comic-fuz.com/series/1583",
 unique_key: "d91z2",
 category: nil,
 use_count: 1,
 expires_at: nil,
 created_at: Sun, 08 Mar 2020 04:31:43 UTC +00:00,
 updated_at: Sun, 08 Mar 2020 04:31:43 UTC +00:00>
```

**○ 6: その他**

```ruby:bash
# Railsアプリケーション内のルーティングへも設定可能
Shortener::ShortenedUrl.generate("/login")

# unique_key を元に登録済みのURLを検索可能
Shortener::ShortenedUrl.fetch_with_token(token: "d91z2")
```


### ■ 設定変更

**○ unique_key の長さを変更**

- デフォルトでは`5`であるが、設定ファイルを生成して、次の通り長さを変更できる。
- しかし、767bytes制限では大きく設定できない(15で失敗確認)。

```ruby:config/initializers/shortener.rb
Shortener.unique_key_length = 10
```

**○ token が一致しない場合のリダイレクト先を変更**

```ruby:config/initializers/shortener.rb
Shortener.default_redirect = "https://www.nintendo.co.jp/switch/acbaa/index.html"
```

**○ unique_key の生成文字を変更**

```ruby:config/initializers/shortener.rb
# デフォルトがこれ
Shortener.charset = :alphanum

# ライブラリ内にある他の設定
## alphanum: ('a'..'z').to_a + (0..9).to_a,
## alphanumcase: ('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a

# 新たに数字やハイフン等を追加できる...らしいが失敗する...
Shortener.charset = ("a".."z").to_a + (0..9).to_a + ["-", "_"]
```

### ■ short_url の可用性

- 短縮 URL を生成する方法として、ヘルパーメソッドも定義されている。
- console で試す場合は、事前に次の通りincludeしておくこと
    - `include Shortener::ShortenerHelper`
    - `include Rails.application.routes.url_helpers`
    - `default_url_options[:host] = "localhost:3000"`
- 参考として、ライブラリのコードを次の通り記載する。

```ruby:
# shortener/app/helpers/shortener/shortener_helper.rb
  def short_url(url, owner: nil, custom_key: nil, expires_at: nil, fresh: false, category: nil, url_options: {})
    short_url = Shortener::ShortenedUrl.generate(
      url,
      owner:      owner,
      custom_key: custom_key,
      expires_at: expires_at,
      fresh:      fresh,
      category:   category
    )

    if short_url
      if subdomain = Shortener.subdomain
        url_options = url_options.merge(subdomain: subdomain)
      end

      options = { controller: :"/shortener/shortened_urls", action: :show, id: short_url.unique_key, only_path: false }.merge(url_options)
      url_for(options)
    else
      url
    end
  end

# shortener/app/models/shortener/shortened_url.rb
  def self.generate!(destination_url, owner: nil, custom_key: nil, expires_at: nil, fresh: false, category: nil)
    # if we get a shortened_url object with a different owner, generate
    # new one for the new owner. Otherwise return same object
    if destination_url.is_a? Shortener::ShortenedUrl
      if destination_url.owner == owner
        destination_url
      else
        generate!(
          destination_url.url,
          owner:      owner,
          custom_key: custom_key,
          expires_at: expires_at,
          fresh:      fresh,
          category:   category
        )
      end
    else
      scope = owner ? owner.shortened_urls : self
      creation_method = fresh ? 'create' : 'first_or_create'

      scope.where(url: clean_url(destination_url), category: category).send(
        creation_method,
        custom_key: custom_key,
        expires_at: expires_at
      )
    end
  end
```

**○ サブドメイン付きでリダイレクト用URLが作成可能**

```ruby:bash
short_url("https://yahoo.co.jp/", url_options: { subdomain: "organization", host: "localhost:3000", protocol: "http" } )
=> "http://organization.localhost:3000/rd/BCDWTVbXwa"
```

**○ 特定のモデルとも関連付けが可能**

```ruby:user.rb
# 関連付けして...
class User < ActiveRecord::Base
  has_shortened_urls
end
```

```ruby:bash
# 生成する！
Shortener::ShortenedUrl.generate("https://comic-fuz.com/series/1583", owner: user)
short_url("https://comic-fuz.com/series/1583", owner: user)
=> #<Shortener::ShortenedUrl:0x00005600092337f0
 id: 6,
 owner_id: 1,
 owner_type: "User",
 url: "https://comic-fuz.com/series/1583",
 unique_key: "IUYIpUvD9O",
 category: nil,
 use_count: 0,
 expires_at: nil,
 created_at: Sun, 08 Mar 2020 07:33:10 UTC +00:00,
 updated_at: Sun, 08 Mar 2020 07:33:10 UTC +00:00>

User.first.shortened_urls.first.url
=> "https://comic-fuz.com/series/1583"
```

**○ unique_key を独自に設定可能**

- 使用時は`unique_key_length`の設定等に注意すること。

```ruby:bash
short_url("https://comic-fuz.com/series/261", custom_key: 'Original')
=> "http://localhost:3000/rd/Original"
```

**○ 短縮URLの有効期限を設定可能**

- 切れたらデフォルト設定のURLへ遷移

```ruby:bash
short_url("https://comic-fuz.com/series/261", expires_at: 1.minutes.since)
```

**○ 短縮URLの再生製が可能**

- デフォルトで、短縮URLはユニークであるが、`fresh: true`オプションで、再生製が可能となる。

```ruby:bash
short_url("https://comic-fuz.com/series/261", fresh: true)
```

**○ 生成される token に使用不可の文字列を設定可能である。**

- 例えば、`http://localhost:3000/rd/terms`と別のURLがルーティングしていた場合、token がその値に生成されたら、ルーティングが競合してしまう。
- そのため、生成してはいけない token を設定可能である。（本記事ではディレクトリを切っているので、競合する心配はない）

```ruby:config/initializers/shortener.rb
Shortener.forbidden_keys.concat %w(terms promo)
```

**○ クローラー等に対する短縮URLカウント**

- デフォルトでは、クローラー等の訪問をカウントしている。
- 次の通り設定することで、無視することができる。

```ruby:config/initializers/shortener.rb
Shortener.ignore_robots = true
```

---
まだ機能があるけど割愛