本稿は「rails5 APIモードでindex.htmlを配信する」の続きです。
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に渡すようにする。
get '*path', to: 'static_pages#spa_forward'
次に、リクエストオブジェクトからSPAで定義しているURLを識別します。
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を返す
3つ方針を考えました。
- URLをそのままにしてindex.htmlを返す
- SPAのURLをクエリパラメーターに退避して、ブラウザでSPAが自力で展開する
- index.htmlにカスタムデータ属性でURLを書き込み、ブラウザでSPAが自力で展開する
1.URLをそのままにしてindex.htmlを返す
もっともシンプルで自然な形です。URLを加工しないので、ブラウザのLocationに/article/1
がセットされ、SPAのルーターがキャッチしたらルーティングを開始します。
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が認知されます。
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にセットします。
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
に戻っています。
{
"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彡゜シェア!シェア!