SPA(single page application)のURLでシェアする

本稿は[「rails5 APIモードでindex.htmlを配信する」](https://qiita.com/studioTeaTwo/items/486eb19c549c32ee3c88)の続きです。
SPA(single page application)のURLでシェアするために少し発展させました。

`https://hoge.com/article/1`をtwitterでシェアし、そのツイートのリンクをクリックして**初めてサイトにアクセスする人がトップページではなくいきなり該当コンテンツを見れる**ようにします。

自由なシェアこそwebの本懐！SSR(server side rendering)を導入しなくてもやれておきたい。

railsとangularでやっています。OGPタグはアプリ共通のためサムネイルの表示などはできません。

# アプローチ

サーバでindex.htmlを返しつつ、ブラウザに到達してからLocationを展開します。
`https://hoge.com/article/1`を例にしていきます。

- railsのルーティングでSPAのURLを認知する
- SPAのURLならパス`/article/1`を保持しつつindex.htmlを返す
- フロントでindex.htmlを展開しつつ該当パス`/article/1`へのルーティングを行う

# railsのルーティングでSPAのURLを認知する

まず、前回は`get '*path', to: redirect('/')`とし、API以外のその他URLを全てリダイレクトしていた箇所をcontrollerに渡すようにする。

```ruby:routes.rb
get '*path', to: 'static_pages#spa_forward'
```

次に、リクエストオブジェクトからSPAで定義しているURLを識別します。

```ruby
class StaticPagesController < ApplicationController

  (...)

  def spa_forward
    # SPAのURLをパターンマッチングする
    if request.original_fullpath =~ /^\/(article|user)(|\/.*)$/
      # SPAのURLの場合の処理を書く
    else
      redirect_to root_url
    end
  end

end
```

# SPAのURLならパス`/article/1`を保持しつつindex.htmlを返す

３つ方針を考えました。

1. URLをそのままにしてindex.htmlを返す
2. SPAのURLをクエリパラメーターに退避して、ブラウザでSPAが自力で展開する
3. index.htmlにカスタムデータ属性でURLを書き込み、ブラウザでSPAが自力で展開する

## 1.URLをそのままにしてindex.htmlを返す

もっともシンプルで自然な形です。URLを加工しないので、ブラウザのLocationに`/article/1`がセットされ、SPAのルーターがキャッチしたらルーティングを開始します。

```ruby
class StaticPagesController < ApplicationController

  (...)

  def spa_forward
    if request.original_fullpath =~ /^\/(article|user)(|\/.*)$/
      render file: 'public/index.html'
    else
      redirect_to root_url # アセットファイルなどのその他はルートに返す
    end  
  end

end
```

しかし、status200を返しつつもindex.htmlが返却されません。response headerがtext/plainだしEtag貼られてるしで、railsのAPIモードが何かしら原因ぽさあるがどうすればいいか調べてもわからず断念。railsでなければ、というかAPIモードで無ければこれで十分のはず...。

## 2.SPAのURLをクエリパラメーターに退避して、ブラウザでSPAが自力で展開する

採用した形式です。URIオブジェクトを起こし、クエリーパラメーターに`to_param`でエンコーディングしたパスをセットします。その後はルート`/`にリダイレクトすれば、ブラウザでは`https://hoge.com/?path=%2Farticle%2F1`でURLが認知されます。

```ruby
class StaticPagesController < ApplicationController

  (...)

  def spa_forward
    if request.original_fullpath =~ /^\/(article|user)(|\/.*)$/
      uri = URI('/')
      # クエリパラメーターにSPAのパス`/article/1`をセットする
      uri.query = { path: request.original_fullpath}.to_param
      # ルートにリダイレクトする
      redirect_to uri.to_s # -> /?path=%2Farticle%2F1
    else
      redirect_to root_url # アセットファイルなどのその他はルートに返す
    end  
  end

end
```

## 3.index.htmlにカスタムデータ属性でURLを書き込み、ブラウザでSPAが自力で展開する

軽くindex.htmlに`tag("div", :data => {:path => @path})`あたりをセットするようなノリで考えたが、やはりAPIモードの壁にあたり断念。やるならstringで自力でviewを作りファイルに書き出しtextで返す感じかな。rails APIモードはネイティブアプリやマイクロサービスのためにあるんやね、webページはほんと持てないね。

# フロントでindex.htmlを展開しつつ該当パス`/article/1`へのルーティングを行う

クエリーパラメーターで渡されたパスがLocationにセットされているので、historyAPIでLocaitonから取り出しデコードして再びLocationにセットします。

```ts:app.component.ts
import { Component } from '@angular/core';
import { Location } from '@angular/common';
import { UrlSerializer } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  constructor(
    private location: Location,
    private urlSerializer: UrlSerializer,
  ) {
    // path文字列が含まれているならLocationを書き換える
    if (this.location.path().match(/path/)) {
      // URLをパースしてクエリーパラメーターをデコードする
      const urlTree = this.urlSerializer.parse(this.location.path());
      // クエリパラーメーターに退避していたSPAのパスをLocationにセットする
      this.location.replaceState(urlTree.queryParams['path']);
    }
  }

}
```

urlTreeオブジェクトの中身はこんなんになっています。`%2Farticle%2F1`が`/article/1`に戻っています。

```json
{
  "root":{
    "segments":[],
    "children":{},
    "parent":null
  },
  "queryParams":{
    "path":"/article/1"
  },
  "fragment":null
}  
```

# できたこと

- ブラウザのアドレスバーに`https://hoge.com/article/1`を手入力して`/article/1`を開ける
- 別サイトのaタグに`https://hoge.com/article/1`を埋め込んで`/article/1`を開ける
- iPhoneアプリのtwitter/LINE/facebookのweb viewで`/article/1`を開ける
- はてなブックマークして`/article/1`を開ける
- mobile Safariのブックマークから`/article/1`を開ける

( ﾟ∀ﾟ)o彡゜シェア！シェア！