🖨️

ブログ内の文章をTシャツにする機能を作った

2023/02/24に公開

SUZURIのエンジニアのyukyuです。SUZURIでは、画像やテキストからTシャツなどのグッズを作成できるSUZURI APIが公開されています。そこで、ブログ内の文章をTシャツにする機能を作りました。
https://suzuri.jp/developer

https://twitter.com/yukyu30/status/1622247556578435072

サイトとソースコード

今回作成したブログとそのソースコードです。ブログの記事はChatGPTで生成したものです。
https://sensational-puffpuff-0189ed.netlify.app/
https://github.com/yukyu30/hakuran-blog

作成動機

ブログの記事という無形物を有体物にできたら、着用することで道行く人にシェアできて面白そうという動機でつくりました。

大まかな仕組み

仕組みとしてはシンプルです。

  1. ブログ上で文章が選択されたら、Tシャツを作るボタンを表示する
  2. ボタンを押したら、SUZURI APIへ選択された文章をPOSTし、Tシャツを作成する
  3. APIのレスポンスでTシャツの販売ページURLを取得し、遷移させる

ブログはNetlifyへデプロイしています。
ボタンが押されたらNetlify Functions(以下、Functions)を介して、SUZURI APIへPOSTしています。
フロントエンドからSUZURI APIを利用するとCORSエラーが起きるため、Functionsを利用しています。

実装

ブログにはGatsby.jsのstarter-blogを利用しました。
https://www.gatsbyjs.com/starters/gatsbyjs/gatsby-starter-blog/

ブログ上で文章が選択された状態の判定

Tシャツを作るボタンは文章が選択されている時のみ表示するので、文章が選択されているかどうか判定する必要があるため、window.getSelection()を利用します。

https://developer.mozilla.org/ja/docs/Web/API/Window/getSelection

window.getSelection().toString()で選択された文字列が取得できるので、この文字列の長さが0より大きい時は文章が選択されていると判断できます。
handleSelectTextで文章が選択されているか判定し、選択されていたら

  • setShowButton(true)でシェアボタンを表示する
  • setShareText(selectedText)shareTextの値を変更する

ということを行いました。

blog-post.js
  const handleSelectText = () => {
    const selectedText = window.getSelection().toString()
    const isSelected = selectedText.length > 0
    if (isSelected) {
      setShowButton(true)
      setShareText(selectedText)
    } else {
      setShowButton(false)
    }
  }
  ...
  <section
	  dangerouslySetInnerHTML={{ __html: post.html }}
	  itemProp="articleBody"
          onMouseUp={() => handleSelectText()}
          onMouseOut={() => handleSelectText()}
   />
 ...

文字選択を行う動作は以下の2パターンが考えられます。

  1. section要素の領域でドラッグして、section要素の領域でボタンを離す
  2. section要素の領域でドラッグして、section要素の領域外でボタンを離す
section要素に限定した意図

section要素に限定しないと、プロフィールの文字やフッターの文字などの記事以外をTシャツにできてしまうためです。

MouseUpはsection内でドラッグして、文字選択を行なっていることを想定し、MouseOutはsection内でドラッグをしていたが、section外へポインタが飛び出していることを想定している。

なので、MouseUp、MouseOut時にhandleSelectTextを実行しています。後述しますが、これだけでは不十分でした。

SUZURI APIでTシャツを作成する

幸いにもSUZURI APIには文章からTシャツを作ることができるエンドポイントがあるので、そちらを利用します。

Material(素材) Text
テキストからMaterialとスタンダードTシャツを作ります。レートリミットが設けてあります。
POST /api/v1/materials/text

https://suzuri.jp/developer

shareTextをPOSTすることでTシャツが作成されます。

APIへPOSTするコードを書く

フロントエンドからFunctionsへPOSTするコードは以下のようになります。

blog-post.js
...
  const handleClickSuzuri = async () => {
    const apiUrl = "https://hoge.netlify.app/.netlify/functions/suzuri"
    const config = {
      headers: {
        "Content-Type": "application/json"
      },
    }
    const data = JSON.stringify({ "text": shareText })

    const response = await axios.post(apiUrl, data, config)
      .then((res) => {
        window.location.href = res.data.products[0].sampleUrl
      })
  }
...

FunctionsからSUZURIへPOSTするコードは以下のようになります。

netlify/functions/suzuri.js
const axios = require('axios')

exports.handler = async (event, context) => {
  const apiUrl = "https://suzuri.jp/api/v1/materials/text"
  const config = {
    headers: {
      "Authorization": `Bearer ${process.env.SUZURI_API_KEY}`,
      "Content-Type": "application/json"
    },
  }

  const data = { "text": JSON.parse(event.body).text }

  const response = await axios.post(apiUrl, data, config)
    .then((res) => {
      return {
        statusCode: res.status,
        body: JSON.stringify(res.data),
      }
    })
    .catch((e) => {
      return {
        statusCode: e.response.status,
        body: JSON.stringify(e.response.data),
      }
    })
  return response
}

Functionsの導入や設定にはこちらを参考にしました
https://qiita.com/Sr_Bangs/items/7867853f5e71bd4ada56

Tシャツの販売ページへ遷移

作成したTシャツ詳細画面へのURLをレスポンスから取得します。Tシャツだけを生成しているのでレスポンスのうち、products[0]にあるsampleUrlがTシャツ詳細画面へのURLなります。そこで、window.location.href = res.data.products[0].sampleUrlで遷移を行うようにしました。


実際につくられたTシャツはこちらで確認できます
https://suzuri.jp/yukyu30-for-api

おわりに

実装後に気づいたのですが、section要素の領域外でドラッグされ続けていて、さらに文章の選択が行われている場合も加味する必要がありそうです。
今回の実装では、スマートフォンではうまく動作しませんでした。
文字選択されている時のみ、ボタンやバルーンなどを表示する実装のベストプラクティスがあったら是非教えていただきたいです。

Discussion