JSON-LDとRails

## TokyuRuby会議11 (2016-07-29) 発表のスライドです
\#tqrk11

---

## @tkawa

- 川村 徹
- プログラマ
- [ソニックガーデン](https://www.sonicgarden.jp/)
- REST厨
- [Sendagaya.rb](https://sendagayarb.doorkeeper.jp/)
  - 毎週月曜 19:30- 
  - ゆるーいRubyコミュニティ

---

## JSON-LDとは

コンピュータが理解できるLinked Dataを表すためのJSONベースのフォーマット

コンピュータが理解できる → Googleクローラが理解できる → リッチな検索結果

Googleにデータの意味を伝えるためのフォーマット（の1つ）として使われることが多い

- BreadcrumbList
- Recipe
- Product
- Article (NewsArticle, BlogPosting)
- Review
- Event
- SoftwareApplication
- ...

AMPでも使う

---

<img width="290" alt="豚肉と茄子の生姜焼き2.png" src="https://qiita-image-store.s3.amazonaws.com/0/6740/c7fa0b6c-4d60-b5a4-9549-4c69961ac1f3.png">

```json
{
  "@context": "http://schema.org/",
  "@type": "Recipe",
  "@id": "https://park.example.co.jp/recipe/card/708850",
  "name": "豚肉と茄子の生姜焼き",
  "image": "https://park.example.co.jp/_var/images/recipe-master/142518/708850.jpg",
  "author": {
    "@type": "Person",
    "name": "キャシィ塚本",
    "image": "https://park.example.co.jp/resources_sp/images/common/cathy.png",
    "description": "四万十川料理専門学校の講師です。"
  },
  "publisher": {
    "@type": "Organization",
    "name": "EXAMPLE株式会社",
    "url": "http://www.example.co.jp/",
    "logo": {
      "@type": "ImageObject",
      "url": "https://park.example.co.jp/resources_sp/images/common/logo_examplepark.png"
    }
  },
  "datePublished": "2017-06-08",
  "dateModified": "2017-06-08",
  "description": "「豚肉と茄子の生姜焼き」のレシピ・作り方です。",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "90",
    "reviewCount": "16",
    "bestRating": "100",
    "worstRating": "80"
  },
  "recipeYield": "2(servings)",
  "totalTime": "PT10M",
  "recipeIngredient": [
    "豚こま切れ肉90g",
    "なす2個(160g)",
    "片栗粉大さじ1",
    "ごま油大さじ1",
    "キャベツのせん切り2枚分(100g)"
  ],
  "recipeInstructions": [
    "（１）なすはタテ５ｍｍ幅の薄切りにする。豚肉、なすの表面に片栗粉をまぶす。",
    "（２）フライパンにごま油を熱し、（１）の豚肉・なすを入れて火が通るまで焼く。",
    "（３）そのままドォーン!!!!"
  ],
  "recipeCuisine": "和風",
  "nutrition": {
    "@type": "NutritionInformation",
    "calories": "248kcal",
    "carbohydrateContent": "20.9g",
    "cholesterolContent": "30mg",
    "fiberContent": "2.6g",
    "proteinContent": "12.9g",
    "sodiumContent": "1,171mg"
  }
}
```

これを `<script type="application/ld+json">` の中に書くという仕様。

---

## さて、Railsでどうやって実装しますか？

---

## 例えばモデルに書く

```rb
# app/models/recipe.rb
def to_jsonld
  {
    '@context': 'http://schema.org/',
    '@type': 'Recipe',
    '@id': recipe_url, # ← ?
    'name': name,
    'image': image_url, # ← ?
    ...
  }.to_json
end
```

URLが出せない（出しづらい） :persevere: 

**なぜならJSONもビューだから**

---

## JSONをビューとして扱う

```rb
# config/initializers/mime_types.rb
Mime::Type.register 'application/ld+json', :jsonld
```

URLに `.jsonld` もしくは
リクエストヘッダ `Accept: application/ld+json` で
jsonldフォーマットのビューテンプレートが使われる。

```rb
# 今回は必要ないが、フォーマットによって処理を変える場合
respond_to do |format|
  format.html { ... }
  format.jsonld { ... }
end
```

---

## テンプレートは [rails/jbuilder](https://github.com/rails/jbuilder) が定番

```rb
# app/views/recipes/show.jsonld.jbuilder
json.set! :@context, 'http://schema.org/'
json.set! :@type, 'Recipe'
json.set! :@id, recipe_url(@recipe)
json.name @recipe.name
json.image image_url(@recipe)
...
```
個人的にはDSLが慣れない。。 :disappointed_relieved: 

---

## そこで [amatsuda/jb](https://github.com/amatsuda/jb)

```rb
# app/views/recipes/show.jsonld.jb
{
  '@context': 'http://schema.org',
  '@type': 'Recipe',
  '@id': recipe_url(@recipe),
  name: @recipe.name,
  image: image_url(@recipe),
  ...
}
```
Hashそのまま書ける！

- 普通にrubyで書く
- 返されたオブジェクトを `to_json` した文字列がrenderされる
- jbuilderより速い

---

## さらに

```rb
# app/views/recipes/show.jsonld.jb
{
  '@context': 'http://schema.org',
  '@type': 'Recipe',
  '@id': recipe_url(@recipe),
  name: @recipe.name,
  image: image_url(@recipe),
  author: {
    '@type': 'Person',
    name: @recipe.author.name,
    image: image_url(@recipe.author),
    description: @recipe.author.description
  },
  ...
}
```

jb は jbuilder と同様にパーシャルが使えるので、ネストした author を……

---

## パーシャルでDRY

```rb
# app/views/recipes/show.jsonld.jb
{
  '@context': 'http://schema.org',
  '@type': 'Recipe',
  '@id': recipe_url(@recipe),
  name: @recipe.name,
  image: image_url(@recipe),
  author: render(@recipe.author),
  ...
}

# app/views/authors/_author.jsonld.jb
{
  '@type': 'Person',
  name: author.name,
  image: image_url(author),
  description: author.description
}
```

recipe 以外のところでも author が再利用可能に

---

## さらにパーシャル化

```rb
# app/views/recipes/show.jsonld.jb
{
  '@context': 'http://schema.org',
  '@id': recipe_url(@recipe)
}.merge(render(@recipe))

# app/views/recipes/_recipe.jsonld.jb
{
  '@type': 'Recipe',
  name: @recipe.name,
  image: image_url(@recipe),
  author: render(@recipe.author),
  ...
}
```

こうすれば recipe 自体も再利用可能（例えば recipe の List など）
HTMLのパーシャルの使い方と同じ

もちろんこの手法はWeb APIでも使えます
JSON-LDもWeb APIとして使えます

- https://json-ld.org/spec/latest/json-ld-api-best-practices/
- https://www.markus-lanthaler.com/hydra/

---

## HTMLに埋め込むには

```rb
# app/helpers/application_helper.rb
def jsonld_script_tag
  jsonld = controller.render_to_string(formats: :jsonld)
  content_tag :script, jsonld.html_safe, type: Mime[:jsonld].to_s
rescue ActionView::ActionViewError => e
  logger.error e.message
  nil
ensure
   # render_to_string のバグ回避 https://github.com/rails/rails/issues/14173
  lookup_context.rendered_format = nil
end

```
`controller.render_to_string` すると、オブジェクトを渡したりしなくても現在のリクエストの状態のままレンダリングした文字列が得られる

---

## HTML以外のものが必要になっても、別のフォーマットのビューと考えてHTMLと同じように作る

---

## どんなフォーマットが来てもうまくいく！