tag:crieit.net,2005:https://crieit.net/tags/OGP/feed 「OGP」の記事 - Crieit Crieitでタグ「OGP」に投稿された最近の記事 2020-05-06T15:52:28+09:00 https://crieit.net/tags/OGP/feed tag:crieit.net,2005:PublicArticle/15887 2020-05-06T15:52:28+09:00 2020-05-06T15:52:28+09:00 https://crieit.net/posts/Canvas-Konva-js-OGP-Nuxt Canvasのライブラリ「Konva.js」でOGP生成(Nuxtアプリ) <p>Nuxt.jsとFirebaseで作っていた<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">Webサービス</a>を7月末にリリースして、はや10ヶ月。。<br /> 登録総額1億円を突破したので、記念企画として<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">積読レビュー</a>機能をリリースしました!!</p> <p>そこで<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>というライブラリを使ってOGP画像を生成したので、<br /> その時に調べたことをまとめてみました。</p> <h3 id="積読・読書前でも書ける『積読レビュー』"><a href="#%E7%A9%8D%E8%AA%AD%E3%83%BB%E8%AA%AD%E6%9B%B8%E5%89%8D%E3%81%A7%E3%82%82%E6%9B%B8%E3%81%91%E3%82%8B%E3%80%8E%E7%A9%8D%E8%AA%AD%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">積読・読書前でも書ける『積読レビュー』</a></h3> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p> <p>読書のレビューというと、読書後の感想を書くものですが、<br /> 積ん読が多いとなかなか書く機会がないです。。</p> <p><strong>「感想は読書後だけではないのでは?」</strong><br /> ということで、読書前でも書けるレビューを2つ用意してみました。</p> <h4 id="妄想で書く『妄想レビュー』"><a href="#%E5%A6%84%E6%83%B3%E3%81%A7%E6%9B%B8%E3%81%8F%E3%80%8E%E5%A6%84%E6%83%B3%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">妄想で書く『妄想レビュー』</a></h4> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/8e8a9968-c44f-64c8-93d2-0f8eb1f3172a.png" width="600px"/></p> <p><strong>まだ読んでないけど、本の表紙や帯の印象から妄想で書いてみる『妄想レビュー』</strong></p> <p>「中身じゃなく外観だけで、読書後のレビューを書いてみるとどうだろう。。?」<br /> というネタ的なレビューです。</p> <p>いかに読んでる醸し出せるかをチャレンジしてみるとおもしろいかも(<em>´ω`</em>)</p> <p>他の使い方としては、<strong>読む前に書いておいて、読んだ後との感想を比較してみる</strong>のもたのしいかも?<br /> 読書前後で感想が同じになっても違っていても、新たな発見があるかもです。</p> <h4 id="きっかけを書く『きっかけレビュー』"><a href="#%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%82%92%E6%9B%B8%E3%81%8F%E3%80%8E%E3%81%8D%E3%81%A3%E3%81%8B%E3%81%91%E3%83%AC%E3%83%93%E3%83%A5%E3%83%BC%E3%80%8F">きっかけを書く『きっかけレビュー』</a></h4> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e740910f-2687-0484-e3d2-bebdd0d95bf6.png" width="600px"/></p> <p><strong>買ったときのきっかけや意気込み・ワクワク感を書いてみる『きっかけレビュー』</strong></p> <p>「『なぜ買うのか、どうして買いたいのか』という気持ちを残しておくのもどうだろう。。?」<br /> と思いつきつくってみました。</p> <p>買ったときの気持ちも大事で、<br /> * 技術書やビジネス書なら「こうなりたい!ここを強くしたい!」<br /> * 小説やマンガなら「ひさびさの新刊!たのしみ!」<br /> という思いがあるはず・・</p> <p><strong>それを記録として残しておくと、いつか見返したときにたのしいかもしれません。</strong></p> <h2 id="なぜKonva.js?"><a href="#%E3%81%AA%E3%81%9CKonva.js%EF%BC%9F">なぜKonva.js?</a></h2> <p>この企画では、本を選んで、レビューを書いていくのですが、<br /> 本の表紙など画像を埋め込む必要があります。</p> <p>以前、<a target="_blank" rel="nofollow noopener" href="http://hen-ai.net/">別のサービス</a>で、<a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2020/01/24/110000">SVGでOGP用の画像を生成</a>してみたのですが、端末によってはうまくいかなかったりと、画像を埋め込むのがかなり大変でした。。</p> <p>なので、SVGではなく、Canvasで試してみようと調べたところ、<br /> <a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>と<a target="_blank" rel="nofollow noopener" href="http://fabricjs.com/">Fabric.js</a>が見つけ、<br /> 文字の折返しなどのガイドがあった<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/">Konva.js</a>を選んでみました。</p> <h2 id="Konvaで画像生成"><a href="#Konva%E3%81%A7%E7%94%BB%E5%83%8F%E7%94%9F%E6%88%90">Konvaで画像生成</a></h2> <p>ここからが本題。やっと本題。。</p> <h3 id="準備"><a href="#%E6%BA%96%E5%82%99">準備</a></h3> <h4 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h4> <pre><code class="shell">$ npm install vue-konva konva --save </code></pre> <p>今回は、Nuxt/Vueで使うので、KonvaのVueライブラリ(<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/vue/index.html">vue-konva</a>)もインストール</p> <h4 id="プラグインの作成"><a href="#%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90">プラグインの作成</a></h4> <p>vue-konvaを使えるようプラグインを作成。</p> <pre><code class="ts">// ~/plugins/vue-konva.ts import Vue from "vue"; import VueKonva from "vue-konva"; Vue.use(VueKonva); </code></pre> <p>そして、作ったプラグインをnuxt.config.tsに追加。</p> <pre><code class="ts">// nuxt.config.js import { Configuration } from "@nuxt/types"; const config: Configuration = { mode: "spa", // 略 plugins: [ { src: "~/plugins/vue-konva", ssr: false }, // 略 ], // 略 }; </code></pre> <p>これで準備はOK</p> <h3 id="KonvaでCanvasを描いてみる"><a href="#Konva%E3%81%A7Canvas%E3%82%92%E6%8F%8F%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">KonvaでCanvasを描いてみる</a></h3> <h4 id="画像を表示する(v-image)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28v-image%29">画像を表示する(v-image)</a></h4> <p>まずは、フレームとなる以下の背景画像を表示してみます。</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/9033133e-55f5-17eb-a8bc-85ce000a68ef.png" width="600px"/></p> <pre><code class="html"><template> <div> <v-stage :config="configKonva"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; @Component export default class ReviewPreviewSample extends Vue { // konva全体の設定(v-stageに対応) private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT }; // 背景画像の設定(v-imageに対応) private configBg: { image: HTMLImageElement | null } = { image: null }; async mounted() { // マウントされたら、背景画像を読み込んで設定する this.setupBg().then(); } // 背景画像の設定 private async setupBg() { const image = await this.getImage("/img/review_paple.png"); this.configBg.image = image; } // 共通処理: 画像の読み込み private async getImage(src: string): Promise<HTMLImageElement> { return new Promise((resolve, reject) => { const image = new window.Image(); image.onload = () => resolve(image); image.src = src; }); } } </script> </code></pre> <p>こんな感じ。</p> <h5 id="基本的な構成"><a href="#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E6%A7%8B%E6%88%90">基本的な構成</a></h5> <p>基本的な構成は、こんな感じで、<br /> <code>v-stage > v-layer > その他諸々</code><br /> で配置していく。</p> <pre><code class="html"><v-stage :config="configKonva"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> </v-stage> </code></pre> <p>必ずrootが<code>v-stage</code>で、その直下が<code>v-layer</code>。<br /> <code>v-layer</code>はいくつも作れるけど、3〜4までがよいっぽい。(waringがでた)</p> <p>位置やサイズ、表示する画像は<code>:config</code>を介して、Kanva.js自体の設定値を付与していく。</p> <p>どんな設定値があるかは、<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>を見ていく感じで、<br /> Imageだと<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.Image.html">ここ(Class: Image)をみる</a></p> <p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/">チュートリアル</a>もあるので、それを見つつ、<br /> 具体的な設定値は<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/api/Konva.html">ApiDoc</a>参照という流れで進めた。</p> <h5 id="画像の表示には読み込みが必要"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%81%AB%E3%81%AF%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%8C%E5%BF%85%E8%A6%81">画像の表示には読み込みが必要</a></h5> <p>画像の表示は少しめんどくさくて、</p> <ol> <li>HTMLImageElementをつくって</li> <li>HTMLImageElementで画像をロードして</li> <li>ロード後のHTMLImageElementをconfigに設定</li> </ol> <p>という流れ。。</p> <p>それをしているのがこのあたり。<br /> 画像読み込みは何度も使うけど、Promiseじゃないので、<br /> ロードが終わったらHTMLImageElementを返す処理を共通化</p> <pre><code class="ts">async mounted() { // マウントされたら、背景画像を読み込んで設定する this.setupBg().then(); } // 背景画像の設定 private async setupBg() { const image = await this.getImage("/img/review_paple.png"); this.configBg.image = image; } // 共通処理: 画像の読み込み private async getImage(src: string): Promise<HTMLImageElement> { return new Promise((resolve, reject) => { const image = new window.Image(); image.onload = () => resolve(image); image.src = src; }); } </code></pre> <p>これでできるのがこんな感じ。</p> <p><img width="600px" alt="スクリーンショット 2020-05-06 12.46.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b295f2ff-7b56-5ab4-1e9c-055ef7ff4314.png"></p> <p>でかい。。<br /> CanvasのサイズをOGP画像のサイズ(1200x630)にしているもんね。。</p> <h5 id="画像を画面サイズにあわせる"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E3%81%82%E3%82%8F%E3%81%9B%E3%82%8B">画像を画面サイズにあわせる</a></h5> <p>ほんとはこんな感じにしたい。。</p> <p><img width="600px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e3210e3f-c4b0-6e6c-1a11-e791dc90dd05.gif" /></p> <p>ので、Windowサイズの変更を検知して、CSSのscaleで調整してみる。</p> <p>こんな感じ。</p> <pre><code class="html"><template> <!-- サイズ計算用にidが必要なので追加 --> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; @Component export default class ReviewPreviewSample extends Vue { private configKonva = { width: IMAGE_WIDTH, height: IMAGE_HEIGHT }; private configBg: { image: HTMLImageElement | null } = { image: null }; private scale: number = 0; async mounted() { this.setupBg().then(); // 初回のスケール計算処理を走らせる this.$nextTick(() => this.handleResize()); // イベントリスナーにスケール計算処理を登録 window.addEventListener("resize", this.handleResize); } beforeDestroy() { // Destroy前に解放 window.removeEventListener("resize", this.handleResize); } private get style() { // transformのscaleを使って、サイズ調整 return { transform: `scale(${this.scale})`, "-webkit-transform": `scale(${this.scale})`, "transform-origin": "0 0" }; } // スケール計算用の処理: 対象IDのサイズを取得して、scaleを計算 private handleResize() { const elm = document.getElementById("preview-wrapper"); if (!elm) return; const rect = elm.getBoundingClientRect(); this.scale = rect.width / IMAGE_WIDTH; } private async setupBg() { // 略 } private async getImage(src: string): Promise<HTMLImageElement> { // 略 } } </script> <style lang="scss" scoped> /* 画像のアスペクト比は固定なので、あらかじめ高さをCSSで調整 */ .preview-wrapper { position: relative; width: 100%; height: auto; } .preview-wrapper { &:before { content: ""; display: block; padding-top: 52.5%; /* 630 / 1200 x 100 */ } } .preview-content { position: absolute; top: 0; left: 0; } </style> </code></pre> <p>これでOK。Konva.jsの話に戻る</p> <h4 id="画像を加工する(サイズ変更)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E3%82%B5%E3%82%A4%E3%82%BA%E5%A4%89%E6%9B%B4%29">画像を加工する(サイズ変更)</a></h4> <p>次は書影を読み込んで、配置する部分。<br /> 書影は本ごとにサイズが違うので、加工が必要。。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_13_06_55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/733c10bc-58e3-ffd4-45cd-09d344c9eb4f.png"></p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <v-layer> <v-image :config="configBg"></v-image> </v-layer> <v-layer> <!-- 書影の画像 --> <v-image :config="configBookImg"></v-image> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; const BOOK_IMAGE_WIDTH = 464; const BOOK_IMAGE_HEIGHT = 470; @Component export default class ReviewPreviewSample extends Vue { // 略 private configBookImg: any | null = null; async mounted() { this.setupBg().then(); // 略 } // 略 private async setupBookImage() { // 画像の読み込み const imageBook = await this.getImage("/img/notfound_paple.png"); // v-imageのconfig const scale = BOOK_IMAGE_WIDTH / imageBook.width; this.configBookImg = { image: imageBook, x: 48, // 左上からの位置(x) y: 112, // 左上からの位置(y) width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横) height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦) crop: { // 切り取り処理 x: 0, // 切り取り位置(x) y: 0, // 切り取り位置(y) width: imageBook.width, // 切り取りサイズ(横) height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦) } }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p>こんな感じ。切り取りは、v-imageのcropを指定すればOK。</p> <p>ここで若干ハマった。。</p> <h5 id="【ハマり1】cropとscaleを同時に指定するとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A1%E3%80%91crop%E3%81%A8scale%E3%82%92%E5%90%8C%E6%99%82%E3%81%AB%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%80%82%E3%80%82">【ハマり1】cropとscaleを同時に指定するとうまくいかない。。</a></h5> <p>サイズを調整しようと、いろいろしていたときに、<br /> スケールを変化させる<code>scale</code>と切り取り処理の<code>crop</code>を同時に指定してみたけど、<br /> あまりうまくいかず、cropのみで対応した。。</p> <h5 id="【ハマり2】computedだとうまくいかない。。"><a href="#%E3%80%90%E3%83%8F%E3%83%9E%E3%82%8A2%E3%80%91computed%E3%81%A0%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%84%E3%80%82%E3%80%82">【ハマり2】computedだとうまくいかない。。</a></h5> <p>最初は、以下のような感じで、computedにしてたけど、<br /> cropなど、配下のobjectが変わっても変更が反映されなかったので、<br /> dataのconfigBookImgを用意する形にしてみた。</p> <pre><code class="ts">get configBookImg() { // 画像の読み込み const imageBook = await this.getImage("/img/notfound_paple.png"); // v-imageのconfig const scale = BOOK_IMAGE_WIDTH / imageBook.width; return { image: imageBook, x: 48, // 左上からの位置(x) y: 112, // 左上からの位置(y) width: BOOK_IMAGE_WIDTH, // 画像のサイズ(横) height: BOOK_IMAGE_HEIGHT, // 画像のサイズ(縦) crop: { // 切り取り処理 x: 0, // 切り取り位置(x) y: 0, // 切り取り位置(y) width: imageBook.width, // 切り取りサイズ(横) height: BOOK_IMAGE_HEIGHT / scale // 切り取りサイズ(縦) } }; } </code></pre> <h4 id="画像を加工する(丸く切り取る)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E5%8A%A0%E5%B7%A5%E3%81%99%E3%82%8B%28%E4%B8%B8%E3%81%8F%E5%88%87%E3%82%8A%E5%8F%96%E3%82%8B%29">画像を加工する(丸く切り取る)</a></h4> <p>次はこれ。ユーザのアイコン画像。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_14_08_51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/80f51e08-b4f4-5fd5-fa3e-35ef560b7197.png"></p> <p>もともとは、こんな四角い画像だけど、アイコンっぽく丸く切り取りたい。</p> <p><img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/62f0de4a-a307-82be-9755-a1a0963b9e2e.png" /></p> <p><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/clipping/Clipping_Function.html">チュートリアル</a>を見てみると、<code>v-group</code>を使うとできるっぽい。</p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <!-- 略 --> <v-layer> <v-group :config="configUserImageGroup"> <v-image :config="configUserImg"></v-image> </v-group> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; const IMAGE_WIDTH = 1200; const IMAGE_HEIGHT = 630; const BOOK_IMAGE_WIDTH = 464; const BOOK_IMAGE_HEIGHT = 470; const USER_IMAGE_SIZE = 76; @Component export default class ReviewPreviewSample extends Vue { // 略 private configUserImg: any | null = null; async mounted() { // 略 this.setupUserImage().then(); // 略 } // 略 private get configUserImageGroup() { return { // 複雑な切り取り処理: CanvasRenderingContext2Dのarcを使う clipFunc: ctx => { const r = USER_IMAGE_SIZE / 2; ctx.arc(1092 + r, 538 + r, r, 0, Math.PI * 2, false); }, // ドラッグを無効にする draggable: false }; } // ユーザ画像の読み込み処理: 本の画像とかとだいたい同じ private async setupUserImage() { const image = await this.getImage("/img/kira_puka.png"); this.configUserImg = { image: image, x: 1092, y: 538, width: USER_IMAGE_SIZE, height: USER_IMAGE_SIZE }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p><code>v-group</code>の<code>clipFunc</code>を使うと、Canvaを操作できるようで、<br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/arc">CanvasRenderingContext2Dのarc</a>を使って、丸く切り取り。</p> <p>注意点は、原点がCanvas(v-stage)の左上であること。<br /> 画像の左上が原点だと思っていたので、全然切り取られなかった。。</p> <h4 id="1行の文字を表示する(省略あり)"><a href="#1%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E3%81%82%E3%82%8A%29">1行の文字を表示する(省略あり)</a></h4> <p>画像はここまで。次は文字。本のタイトルを表示したい。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_14_20_00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/5042be5a-3901-780e-34eb-3616558ed1e8.png"></p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <!-- 略 --> <v-layer> <v-text :config="configBookTitle"></v-text> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; // 略 @Component export default class ReviewPreviewSample extends Vue { // 略 private get configBookTitle() { return { // 表示する文字 text: "選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル 選んだ本のタイトル", x: 200, y: 28, width: IMAGE_WIDTH - 400, fill: "#FFFFFF", // 文字の色 fontSize: 36, // フォントサイズ fontFamily: "Noto Sans JP", // フォントの種類 fontStyle: "bold", // フォントのスタイル align: "center", // 揃え位置(中央揃え) // 折り返しなし(wrap)を指定すると、省略(ellipsis)を設定可能に wrap: "none", ellipsis: true }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p>文字を表示するには、<code>v-text</code>を使えばOK</p> <p>CSSのtext-overflowと同じ感じで、折返しをnoneにすれば、省略記号の表示もできる。</p> <h4 id="複数行の文字を表示する(省略不可)"><a href="#%E8%A4%87%E6%95%B0%E8%A1%8C%E3%81%AE%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28%E7%9C%81%E7%95%A5%E4%B8%8D%E5%8F%AF%29">複数行の文字を表示する(省略不可)</a></h4> <p>次はレビューの文字を表示。</p> <p><img width="600" alt="スクリーンショット_2020-05-06_14_30_22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/75fc126e-1179-e8d1-a2ab-6aa1aaf44974.png"></p> <pre><code class="html"><template> <div id="preview-wrapper" class="preview-wrapper"> <v-stage :config="configKonva" class="preview-content" :style="style"> <!-- 略 --> <v-layer> <v-text :config="configReview"></v-text> </v-layer> </v-stage> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; // 略 @Component export default class ReviewPreviewSample extends Vue { // 略 private get configReview() { return { text: "レビューの内容れびゅーのないよう\n\nレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないようレビューの内容れびゅーのないよう", x: 596, y: 188, width: 542, height: 270, fill: "#443a33", fontSize: 30, fontFamily: "Noto Sans JP" // 行の高さ lineHeight: 1.2, }; } // 略 } </script> <style lang="scss" scoped> /* 略 */ </style> </code></pre> <p>同じく<code>v-text</code>を使う感じ。<br /> <code>wrap</code>を指定しなければ、そのまま複数行に。</p> <p>改行文字を入れれば改行もしてくれて、widthを指定すれば折返しも自動。</p> <p>ただ、残念ながら、<code>-webkit-line-clamp</code>みたいに複数行の省略はないみたい。。</p> <h3 id="Canvasを画像にエクスポート"><a href="#Canvas%E3%82%92%E7%94%BB%E5%83%8F%E3%81%AB%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9D%E3%83%BC%E3%83%88">Canvasを画像にエクスポート</a></h3> <p>最後に作った画像をエクスポート!!</p> <p><img width="500" alt="スクリーンショット_2020-05-06_14_41_16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/57be76de-b8c2-3a34-31f8-5b85079b621c.png"></p> <pre><code class="html"><template> <div> <div id="preview-wrapper" class="preview-wrapper"> <!-- v-stageが取得できるようにrefを追加--> <v-stage ref="stage" :config="configKonva" class="preview-content" :style="style" > <!-- 略 --> </v-stage> </div> <!-- ボタンを追加 --> <div class="has-text-centered"> <b-button type="is-primary" size="is-medium" @click="download"> <span>作成する</span> </b-button> </div> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Watch } from "nuxt-property-decorator"; // 略 @Component export default class ReviewPreviewSample extends Vue { // 略 // ダウンロード処理 private async download() { // stageの取得: 大きいサイズを取得するときは、'pixelRatio'で倍率を指定 const stage = (this.$refs.stage as any).getStage({ pixelRatio: 1 }); // stageからDataURLを取得 const dataURL = stage.toDataURL(); // ダウンロード処理: プログラムでリンクを作ってクリック const link = document.createElement("a"); link.download = "ogp.png"; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); } } </script> <style lang="scss" scoped> /** 略 **/ </style> </code></pre> <p>こんな感じ。Konvaでエクスポートするときには、<code>v-stage</code>から取得する。<br /> DataURL以外にJSONとかでもエクスポートできるので、保存したりもOK</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/data_and_serialization/Stage_Data_URL.html">HTML5 Canvas Stage Data URL Tutorial | Konva - JavaScript 2d canvas library</a></li> </ul> <p>開発しているサービスでは、FirebaseのCloud Storageに保存しているので、DataURL形式でOK。</p> <p>ハマリポイントは、外部URLの画像を使っているとき。。エクスポートに失敗する。。<br /> Cavans自体の仕様で外部URLの画像がある場合、汚染されたとみなされエラーになるとのこと。。</p> <p>対処法は以下に書かれている感じで、CORS対応をすればOK<br /> ・<a target="_blank" rel="nofollow noopener" href="https://konvajs.org/docs/posts/Tainted_Canvas.html">Resolving "Tainted canvases may not be exported" with Konva | Konva - JavaScript 2d canvas library</a></p> <h2 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h2> <p>SVGでも外部URLは問題があったけど、Konva.jsでも。。<br /> でも、サクサクできたのと、ドラッグで位置を変えれるみたいなので、いろいろできそう(<em>´ω`</em>)</p> <p>積ん読レビューはできたての機能なので、ぜひぜひさわってみてくださいヽ(=´▽`=)ノ<br /> ■<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site/review/">どっちでレビューする?妄想レビュ・きっかけレビュー</a></p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/fc0f21b7-ee49-f01c-34a1-0d44ed3022d2.png" width="600px"/></p> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15766 2020-03-14T22:03:11+09:00 2020-03-14T22:04:12+09:00 https://crieit.net/posts/1bbf25a7cba4095f0fd1afe817b553dd (改良版)パワプロ風画面ジェネレータを作ってみた <p><a href="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102e92a702e89059033b1dff5b0f87c55e6cd23db0d46.jpg?mw=700" alt="" /></a></p> <h1 id="作ったサービス"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9">作ったサービス</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://pawapro-gen.netlify.com/">https://pawapro-gen.netlify.com/</a></p> <h1 id="一旦書いたボード"><a href="#%E4%B8%80%E6%97%A6%E6%9B%B8%E3%81%84%E3%81%9F%E3%83%9C%E3%83%BC%E3%83%89">一旦書いたボード</a></h1> <p><a href="https://crieit.net/boards/web1week-202003/6fbd2929361eb9caa53fc33d51d31f36">web1week - パワプロ風画面ジェネレータを作ってみた</a></p> <p>時間的に残りの機能間に合わないかと思っていたので、最低限動く機能で出したのですが、結論から言うと間に合いました。</p> <h1 id="インライン編集機能"><a href="#%E3%82%A4%E3%83%B3%E3%83%A9%E3%82%A4%E3%83%B3%E7%B7%A8%E9%9B%86%E6%A9%9F%E8%83%BD">インライン編集機能</a></h1> <p>当初はフォームとプレビュー機能を分けて作っていたのですが、<br /> ほとんど全てインラインで編集できるように変更しました。</p> <h1 id="twitterシェア機能"><a href="#twitter%E3%82%B7%E3%82%A7%E3%82%A2%E6%A9%9F%E8%83%BD">twitterシェア機能</a></h1> <h2 id="OGPサーバの構築"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89">OGPサーバの構築</a></h2> <p>以前<a href="https://crieit.net/posts/slack">slack流量計</a>を作った際に、web表彰のスクリーンショットを撮っていたので、OGP作成にはそれほど抵抗はありませんでした。</p> <p>あとはだらさんの作成されてる<a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/puppeteer-ogp">OGPサーバ</a>を参考にさせていただきました。</p> <h3 id="OGPサーバの構築に使った技術"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E6%A7%8B%E7%AF%89%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">OGPサーバの構築に使った技術</a></h3> <ul> <li>NodeJS</li> <li>Express</li> <li>TypeScript</li> <li>puppeteer</li> <li>VPS</li> <li>docker-compose</li> </ul> <h2 id="ロジック"><a href="#%E3%83%AD%E3%82%B8%E3%83%83%E3%82%AF">ロジック</a></h2> <ul> <li>ジェネレータからDBにデータを登録する</li> <li>登録されたデータをもとにpuppeteerでスクリーンショットを撮る</li> <li>DBに画像を格納する</li> <li>OGPのサーバ問い合わせにはDBに保存してある画像を使用する <ul> <li>OGPサーバに直接アクセスさせると負荷が集中してサーバが落ちるため</li> </ul></li> </ul> <h3 id="DB登録処理は既存のAPサーバに追加"><a href="#DB%E7%99%BB%E9%8C%B2%E5%87%A6%E7%90%86%E3%81%AF%E6%97%A2%E5%AD%98%E3%81%AEAP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AB%E8%BF%BD%E5%8A%A0">DB登録処理は既存のAPサーバに追加</a></h3> <h3 id="OGPサーバは別コンテナ"><a href="#OGP%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AF%E5%88%A5%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A">OGPサーバは別コンテナ</a></h3> <h2 id="twitter投稿"><a href="#twitter%E6%8A%95%E7%A8%BF">twitter投稿</a></h2> <p>アプリ起動時<code>componentDidMount</code>のタイミングでUUIDを生成するようにしました。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/react-share">react-share</a>というライブラリを使い、<br /> <code>beforeOnClick</code>メソッドでDBへの登録を行っています。<br /> ローディング画面も作りました(同時にバグ生成)。</p> <pre><code class="html"><TwitterShareButton url={'https://pawapro-gen.netlify.com/view/'+store.getState().uuid} hashtags={['パワプロ風画面ジェネレータで作ってみた']} beforeOnClick={async()=>{ store.dispatch(switchLoading(true)); let isSuccess = await postData(); store.dispatch(switchLoading(false)); if(!isSuccess){ alert('データの保存に失敗しました。時間を空けるか、リロードして再度作成してください。') return new Error(); }else{ return Promise.resolve(); } <span>}</span><span>}</span> > 保存してシェアする! </TwitterShareButton> </code></pre> <h1 id="苦労したこと"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">苦労したこと</a></h1> <h2 id="画像のキャッシュ"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5">画像のキャッシュ</a></h2> <p>ヘッダでキャッシュコントロールしたことがなかったので若干はまりました。</p> <h2 id="OGPのトリミング問題"><a href="#OGP%E3%81%AE%E3%83%88%E3%83%AA%E3%83%9F%E3%83%B3%E3%82%B0%E5%95%8F%E9%A1%8C">OGPのトリミング問題</a></h2> <p>twitterやFacebookでOGPの上下左右がトリミングされる問題があり、<br /> Jimpなどで余白の拡張をしようと思ったのですが、時間がなかったこともあり、<br /> React側でマージンをとってスクリーンショットを撮る際に余白ができるように修正しました。</p> <h1 id="できなかったこと"><a href="#%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">できなかったこと</a></h1> <h2 id="チーム・選手画像のアップロード"><a href="#%E3%83%81%E3%83%BC%E3%83%A0%E3%83%BB%E9%81%B8%E6%89%8B%E7%94%BB%E5%83%8F%E3%81%AE%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89">チーム・選手画像のアップロード</a></h2> <p>OGPの生成で手一杯だったので、base64化してDBに格納するところまで考えられませんでした。後々追加する予定。</p> ckoshien tag:crieit.net,2005:PublicArticle/15723 2020-02-19T19:53:57+09:00 2020-02-19T19:53:57+09:00 https://crieit.net/posts/Cloud-Function-SVG-OGP Cloud FunctionとSVGでOGP画像生成を試行錯誤したまとめ <p>Nuxt+Firebaseで<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">開発してるサービス</a>のOGP画像を改善しようと、<br /> いろいろ試してみたときの備忘録。</p> <p>OGP画像の生成はクライアント側とサーバ側かがあるが、<br /> Firestoreの変更に合わせて生成したいので、<br /> Cloud Function上で利用できる方法を考えてた。</p> <p>結果的に、</p> <ol> <li><a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp</a>で画像を合成してベースをつくり</li> <li>ImageMagickで文字を追記していく</li> </ol> <p>という構成になった。その試行錯誤の備忘録です。</p> <h3 id="やりたかったこと"><a href="#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">やりたかったこと</a></h3> <p>背景画像+本の画像+文字みたいなOGP画像にしたい。</p> <p><a href="https://crieit.now.sh/upload_images/8b51de5926bba4fd12714eb7d06f4d8b5e4d1387cde01.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8b51de5926bba4fd12714eb7d06f4d8b5e4d1387cde01.png?mw=700" alt="tsundoku_book_start_4873115655.png" /></a></p> <p>あと、座標の指定はめんどくさいので、楽な方法を探してみた。<br /> SVGだとCSSも使えて、ブラウザ上で書くにできるので良さそう。<br /> (と思ったけど、結果、だめだった...)</p> <h3 id="ためした方法とまとめ"><a href="#%E3%81%9F%E3%82%81%E3%81%97%E3%81%9F%E6%96%B9%E6%B3%95%E3%81%A8%E3%81%BE%E3%81%A8%E3%82%81">ためした方法とまとめ</a></h3> <p>試したのは以下の4パターン。<br /> SVG+<a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp</a>で試した感じ。<br /> node-canvasはおまけな感じ程度。</p> <ol> <li>sharp+SVG(style+foreginObject) <ul> <li>SVGですべて構成する方法。</li> <li>フォントとか位置、サイズはstyleで設定</li> <li>外部URLの画像が取得できずにNG</li> </ul></li> <li>sharp+SVG+画像は別で合成 <ul> <li>1.の画像部分を別で合成するパターン</li> <li>カスタムフォントが設定できずNG</li> </ul></li> <li>sharp+SVG+ImageMagick <ul> <li>画像の合成部分のみsharpをつかい</li> <li>枠線をSVG、文字はImageMagickで書き出し</li> <li>これを採用</li> </ul></li> <li>node-canvas+SVG <ul> <li>node-canvasもSVGを入力にできるようなので試した</li> <li>1.や2.と同様の理由でNG</li> </ul></li> </ol> <p>内部で利用しているSVGライブラリ自体で未対応っぽいので、<br /> SVGのstyleや外部URLの画像、カスタムフォントなどは、<br /> ブラウザ上以外の画像生成ではまだ未対応っぽい。</p> <p>ほかにも、CloudFunction上でpuppeteerを使えるようだけど、<br /> メモリをすごい使うっぽい記事を見てしまい、まだためしてない。。</p> <hr /> <p>以下試したコード。</p> <h4 id="SVG(style+foreginObject)"><a href="#SVG%28style%2BforeginObject%29">SVG(style+foreginObject)</a></h4> <ul> <li>sharpが、foreignObject+sytleに未対応...のためNG</li> <li>imageに外部URLを使っているとダメっぽい...</li> </ul> <pre><code class="typescript">import sharp from "sharp"; await sharp("./input.svg") .png() .toFile("./output.png"); </code></pre> <pre><code class="xml"><?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 1200 630" width="1200" height="630"> <style> @import url("https://fonts.googleapis.com/css?family=Noto+Sans+JP:500&amp;display=swap&amp;subset=japanese"); .contents { display: flex; justify-content: center; align-items: flex-end; width: 1200px; height: 630px; position: relative; } .book { height: calc(100% - 100px); } .text-contents { position: absolute; left: 0; right: 0; bottom: 58px; width: 1200px; background-color: rgba(0, 0, 0, 0.6); font-family: "Noto Sans JP", sans-serif; color: white; padding-bottom: 8px; text-align: center; } .text-label { font-size: 60px; font-weight: 500; padding-left: 0.5em; } .title-label { font-size: 28px; font-weight: 500; padding-top: 0.2em; } </style> <image xlink:href="https://.../background.png" width="1200" height="630" /> <foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" width="1200" height="630"> <div class="contents"> <img class="book" src="https://.../thumbnail.png" /> <div class="text-contents"> <div class="text-label"> <span>読みます!!</span> </div> <div class="title-label"> <span>リーダブルコード</span> </div> <div class="title-label"> <span>ダスティン・ボズウェル/トレバー・フォシェ</span> </div> </div> </div> </foreignObject> </svg> </code></pre> <h4 id="SVG+外部URLの画像"><a href="#SVG%2B%E5%A4%96%E9%83%A8URL%E3%81%AE%E7%94%BB%E5%83%8F">SVG+外部URLの画像</a></h4> <ul> <li>SVGには文字とかだけにして、画像はファイル読み込みで対応</li> <li>カスタムフォントが使えない...のNG</li> <li>GAEかlamdaだと、fontconfigに関する環境変数を設定できるが、Cloud Functionでは無理そう</li> <li>fonts.confを設定してもダメだった...</li> </ul> <pre><code class="typescript">import axios from "axios"; import sharp from "sharp"; // 埋め込む画像のURL const bookURL = "https://.../thumbnail.png"; const bookBuffer = await axios.get(bookURL, { responseType: "arraybuffer" }); // URLから取得した画像を加工(リサイズ) const bookImage = await sharp(bookBuffer.data) .resize(520, 454, { position: "top" }) .toBuffer(); // sharpで結合 await sharp("./background.png") // 背景画像を読み込み .composite([ { input: bookImage, gravity: "south" }, 本の画像を書き出し { input: "./input.svg" } // SVGの書き出し ]) .png() .toFile(tempFile); </code></pre> <pre><code class="xml"><?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 1200 630" width="1200" height="630"> <defs> <!-- Fontを外部URLで指定 --> <font-face font-family="Noto Sans JP"> <font-face-src> <font-face-uri xlink:href="https://fonts.googleapis.com/css?family=Noto+Sans+JP:400,700|Roboto:400,700&amp;display=swap&amp;subset=japanese" /> </font-face-src> </font-face> <!-- フォントをローカルファイルで指定 --> <font-face font-family="Noto Sans JP" font-weight="500"> <font-face-src> <font-face-uri xlink:href="./font/NotoSansJP-Medium.otf"> <font-face-format string="opentype"/> </font-face-uri> </font-face-src> </font-face> <font-face font-family="Noto Sans JP" font-weight="700"> <font-face-src> <font-face-uri xlink:href="./font/NotoSansJP-Bold.otf"> <font-face-format string="opentype"/> </font-face-uri> </font-face-src> </font-face> <!-- ローカルファイルを@font-faceで指定 --> <style type="text/css"> @font-face { font-family: 'Noto Sans JP'; font-style: normal; font-weight: 500; src: url('./font/NotoSansJP-Medium.otf') format("opentype"); } @font-face { font-family: 'Noto Sans JP'; font-style: normal; font-weight: 700; src: url('./font/NotoSansJP-Bold.otf') format("opentype"); } </style> </defs> <rect x="0" y="530" width="1200" height="100" fill="#000000" fill-opacity="0.6" /> <text x="610" y="156" font-size="60" fill="#FFFFFF" text-anchor="middle"> <tspan font-weight="700">読みます!!</tspan> </text> <text x="600" y="574" font-size="28" fill="#FFFFFF" text-anchor="middle"> <tspan font-weight="700">リーダブルコード</tspan> </text> <text x="600" y="610" font-size="24" fill="#FFFFFF" text-anchor="middle"> <tspan font-weight="500">ダスティン・ボズウェル/トレバー・フォシェ</tspan> </text> </svg> </code></pre> <h3 id="SVG+ImageMagick"><a href="#SVG%2BImageMagick">SVG+ImageMagick</a></h3> <ul> <li>画像の加工や合成はsharpを使い、文字の書き出しだけImageMagickを使う</li> <li>画像の加工・合成部分は、上の「SVG+外部URLの画像」と同じ感じ</li> <li>ImageMagickで文字を書くのは以下の記事を参照 <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.memory-lovers.blog/entry/2019/06/26/194500" target="_blank" rel="noopener">Cloud Functions + ImageMagickでOPG画像の動的生成してCloud Storageにアップロードする - くらげになりたい。</a></li> </ul></li> </ul> <h3 id="node-canvas+SVG"><a href="#node-canvas%2BSVG">node-canvas+SVG</a></h3> <ul> <li>同じくダメ。imageタグの外部URLがNG+foreignObject未対応...</li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/Automattic/node-canvas">Automattic/node-canvas: Node canvas is a Cairo backed Canvas implementation for NodeJS.</a></li> </ul> <h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2> <p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br /> <a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p> <p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p> <p>要望・感想・アドバイスなどあれば、<br /> 公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p> <h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://blog.kozakana.net/2019/04/sharp-resize/">Node.jsのライブラリsharpでリサイズを試してみる | Simple is Beautiful.</a></li> <li><a target="_blank" rel="nofollow noopener" href="http://cream-worker.blog.jp/archives/1073895056.html">くりーむわーかー : axiosで取得した画像データを保存とか表示とかする</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15695 2020-01-24T15:25:45+09:00 2020-01-24T15:25:45+09:00 https://crieit.net/posts/SVG-OGP-PNG-PNG SVGでOGP用のPNG画像を生成してみる(折り返し文字、画像埋め込み、PNG化) <p>SVGでOGP画像を作りたいなと思い、いろいろ調べたときの備忘録。</p> <p>SVGはまるのも多いけど、いろいろできるので、<br /> SVGからOGP画像をつくるのいいかもしれない。</p> <h2 id="とりあえず、SVGを書いてみる"><a href="#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E3%80%81SVG%E3%82%92%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">とりあえず、SVGを書いてみる</a></h2> <p>ちょろっとなるなら、HTMLでベタ書きしてみるのもいい。<br /> 動作確認とか楽ちん。</p> <pre><code class="html"><html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg> </body> </html> </code></pre> <p>こんな感じ。</p> <p><a href="https://crieit.now.sh/upload_images/0996c11f41d0943bf90efa5baebbc3af5e2a8d8a9963d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0996c11f41d0943bf90efa5baebbc3af5e2a8d8a9963d.png?mw=700" alt="スクリーンショット 2020-01-23 16.15.07.png" /></a></p> <p><code><rect></code>で四角を書いて、<code><circle></code>で丸を書いてる。</p> <p>上から順に描画されるので、下にある方が前面にくる感じ。<br /> なので、四角の上に丸が描画されてる。</p> <p><code><rect></code>の座標(x,y)は左上の場所なので、<br /> 横幅と縦幅分の半分だけ、全体の中心からずらしてる。</p> <h2 id="画像化する"><a href="#%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">画像化する</a></h2> <p>最終的にOGPで使うPNG画像にしたい。</p> <p>画像化する方法について、ローカルとブラウザ上の2つを試した。</p> <h4 id="1. ローカルで画像化する"><a href="#1.+%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%81%A7%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">1. ローカルで画像化する</a></h4> <p>いろんな画像フォーマットに対応しているsharpがよさそう。<br /> ・<a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp - High performance Node.js image processing</a></p> <p>.svgファイルを.pngに簡単にできる。</p> <h5 id="sharpでsvgをpngに変換してみる"><a href="#sharp%E3%81%A7svg%E3%82%92png%E3%81%AB%E5%A4%89%E6%8F%9B%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B">sharpでsvgをpngに変換してみる</a></h5> <p>まずは、.svgとして扱えるように、<br /> xmlnsとかをちゃんとつけたsample.svgを用意する。</p> <pre><code class="xml"><!-- sample.svg --> <?xml version="1.0"?> <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 1200 630" width="1200" height="630" fill="lightgray"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg> </code></pre> <p>パッケージをインストール</p> <pre><code class="shell">$ npm i sharp </code></pre> <p>変換するコードはこんな感じ。</p> <pre><code class="javascript">// generate.js const sharp = require("sharp"); async function main() { await sharp("sample.svg") .png() .toFile("output.png"); } main().then(); </code></pre> <p>そして、実行。</p> <pre><code class="shell">$ node generate.js </code></pre> <p>すると、output.pngというPNGファイルを作成してくれる。</p> <p>ただ、<strong><code>svg</code>のstyleは反映してくれないので注意</strong>。</p> <pre><code class="javascript">// generate.js const sharp = require("sharp"); async function main() { await sharp("sample.svg") .png() .toFile("output.png"); } main().then(); </code></pre> <p>背景色をつける場合は、sharp側で設定すればOK</p> <pre><code class="javascript">// generate.js const sharp = require("sharp"); async function main() { await sharp("sample.svg") .flatten({ background: "lightgray" }) // 背景色 .png() .toFile("output.png"); } main().then(); </code></pre> <h3 id="2. ブラウザ上のSVGを画像化する"><a href="#2.+%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%B8%8A%E3%81%AESVG%E3%82%92%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B">2. ブラウザ上のSVGを画像化する</a></h3> <p>ユーザに情報を入力してもらった内容をSVGで表示して、<br /> OGP画像にするという感じが多いので、こっちがメイン。</p> <p>保存先のFirebase StorageがData URL形式に対応しているので、<br /> PNGのData URLが取得できればOK。</p> <p>大まかな流れは、こんな感じ。</p> <ol> <li>Canvasを用意する</li> <li>SVGを読み込む<code><image></code>を用意する</li> <li><code><svg></code>を文字列に変換</li> <li>作成した<code><image></code>にDataURL形式でSVGをセットして、読み込み開始</li> <li>読み込みが完了したら、Canvasに書き出して</li> <li>CanvasからDataURLを取得する</li> </ol> <pre><code class="typescript">// svg2DataURL.ts /** * svgをpngに変換 * @param svgElement <svg>のHTML要素 */ export default function svg2DataURL( svgElement: HTMLElement ): Promise<HTMLCanvasElement> { return new Promise((resolve, reject) => { // 1. Canvasを用意する const canvas = document.createElement("canvas"); canvas.width = 1200; canvas.height = 630; const ctx = canvas.getContext("2d"); if (!ctx) { reject(Error("Create Canvas Error...")); return; } // 2. SVGを読み込む<image>を用意する const image = new Image(); image.decoding = "async"; image.onload = () => { // 5. 読み込みが完了したら、Canvasに書き出して、 ctx.drawImage(image, 0, 0, 1200, 630); // 6. CanvasからDataURLを取得する resolve(canvas.toDataURL()); }; image.onerror = e => reject(e); // 3. <svg>を文字列に変換 const svgXml = new XMLSerializer().serializeToString(svgElement); const svgData = btoa(unescape(encodeURIComponent(svgXml))); // 4. 作成した<image>にDataURL形式でセットして、読み込み開始 image.src = `data:image/svg+xml;charset=utf-8;base64,${svgData}`; }); } </code></pre> <p>あとは、好きなタイミングで呼び出せばOK。</p> <p>Nuxt.jsでの例はこんな感じ。</p> <pre><code class="html"><template> <div> <!-- document.getElementByIdできるように、idをつけておく --> <svg id="svg" viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg> </div> <div> <a class="button" @click="saveSVG">画像を保存</a> </div> </template> <script lang="ts"> import { Component, Vue } from "nuxt-property-decorator"; // 初期化済みのfirebaseインスタンス。詳細は略 import firebase from "~/plugins/firebase"; import svg2DataURL from "./svg2DataURL"; @Component() export default class SaveSvgPage extends Vue { // 画像を保存する処理 async saveSVG() { // svgのHTML要素を取得 const elm = document.getElementById("svg"); if (!elm) return; // さっきの処理: HTML要素からDataURLを取得 const dataURL = await svg2DataURL(elm); // Cloud Storage for Firebaseへ保存 const filePath = "...保存する先のパス..." const storage = firebase.storage(); const fileRef = storage.ref().child(filePath); await fileRef.putString(dataURL, "data_url"); } </script> </code></pre> <p>これでSVGがCloud Storage for FirebaseにPNG画像で保存できる(<em>´ω`</em>)</p> <h4 id="ハマったポイント"><a href="#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88">ハマったポイント</a></h4> <h5 id="1. 画像化するときにhtml2canvasを使うといいかんじにならない"><a href="#1.+%E7%94%BB%E5%83%8F%E5%8C%96%E3%81%99%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%ABhtml2canvas%E3%82%92%E4%BD%BF%E3%81%86%E3%81%A8%E3%81%84%E3%81%84%E3%81%8B%E3%82%93%E3%81%98%E3%81%AB%E3%81%AA%E3%82%89%E3%81%AA%E3%81%84">1. 画像化するときにhtml2canvasを使うといいかんじにならない</a></h5> <p>画像化する方法で<a target="_blank" rel="nofollow noopener" href="https://html2canvas.hertzen.com/">html2canvas</a>もあるけど、うまくいかなかった。。</p> <p>スクロール位置や画面サイズによって、<br /> うまく撮れるときと撮れない時があって、この方法に。。</p> <h5 id="2. ブラウザ上と保存した画像が違う"><a href="#2.+%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%B8%8A%E3%81%A8%E4%BF%9D%E5%AD%98%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%81%8C%E9%81%95%E3%81%86">2. ブラウザ上と保存した画像が違う</a></h5> <p>この後出てくる小ネタ集でスタイルで装飾する方法を使ったところ、<br /> うまくいかない感じに。。</p> <p>svg内にsytleをもたせたらうまくいった(<em>´ω`</em>)</p> <p>ブラウザ上 = 全体のCSSが適用<br /> 保存画像 = SVG配下のCSSのみ適用</p> <p>なので、SVGだけで完結しないといけない...</p> <p>フォントとかも指定しないと、見た目が変わってしまう。。</p> <h5 id="3. 背景画像など外部リンクがあるとうまく保存できない..."><a href="#3.+%E8%83%8C%E6%99%AF%E7%94%BB%E5%83%8F%E3%81%AA%E3%81%A9%E5%A4%96%E9%83%A8%E3%83%AA%E3%83%B3%E3%82%AF%E3%81%8C%E3%81%82%E3%82%8B%E3%81%A8%E3%81%86%E3%81%BE%E3%81%8F%E4%BF%9D%E5%AD%98%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84...">3. 背景画像など外部リンクがあるとうまく保存できない...</a></h5> <p>Canvasの仕様っぽく、外部リンクでCORSで引っかかると画像が表示されないよう...</p> <p>SVGで完結するように、読み込んだ画像をDataURL形式で指定するようにしたらうまくいった(<em>´ω`</em>)</p> <p>「<strong>SVGで完結</strong>」が大事らしい。。</p> <h2 id="小ネタ集"><a href="#%E5%B0%8F%E3%83%8D%E3%82%BF%E9%9B%86">小ネタ集</a></h2> <h3 id="svg内でstyleが使える"><a href="#svg%E5%86%85%E3%81%A7style%E3%81%8C%E4%BD%BF%E3%81%88%E3%82%8B">svg内でstyleが使える</a></h3> <p><code><style></code>タグがあるらしい。<br /> classを設定して、いろいろできる。便利。</p> <pre><code class="html"><html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <style> .rect { fill: green; } </style> <rect class="rect" x="600" y="315" width="100" height="100"/> </svg> </body> </html> </code></pre> <h3 id="折り返し文字を表示してみる(foreignObject)"><a href="#%E6%8A%98%E3%82%8A%E8%BF%94%E3%81%97%E6%96%87%E5%AD%97%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B%28foreignObject%29">折り返し文字を表示してみる(foreignObject)</a></h3> <p>SVGにも<code><text></code>があるけど、自動折り返しに対応していない。<br /> 文字数を計算して自分で分割すればできるけど、めんどくさい。。</p> <p>文字の折り返ししたい場合、<code><foreignObject></code>という<br /> HTMLを追加できるのがあるので、それを使うといいらしい。</p> <p>styleでfont-sizeとかも設定できるのでいろいろできそう(<em>´ω`</em>)</p> <pre><code class="html"><html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <style> @import url("https://fonts.googleapis.com/css?family=Noto+Sans+JP:500&display=swap&subset=japanese"); .item { font-family: "Noto Sans JP", sans-serif; font-size: 60px; border: 2px solid green; } </style> <foreignObject requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" width="200" height="200" x="500" y="215" > <div class="item"> こんにちは </div> </foreignObject> </svg> </body> </html> </code></pre> <p>こんな感じ</p> <p><a href="https://crieit.now.sh/upload_images/c4318acbc1f8d438ed7eea4a3e11257a5e2a8da3252f2.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c4318acbc1f8d438ed7eea4a3e11257a5e2a8da3252f2.png?mw=700" alt="スクリーンショット 2020-01-23 17.44.58.png" /></a></p> <p>ただ、foreignObjectはサイズを自動計算してくれるわけではないので、<br /> widthとheightを設定しないといけない。</p> <h3 id="画像を表示する(image)"><a href="#%E7%94%BB%E5%83%8F%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28image%29">画像を表示する(image)</a></h3> <p>SVGで画像を使うときは、<code><image></code>を使う。<br /> <code><img></code>とは違う。</p> <pre><code class="html"><html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <image xlink:href="https://mdn.mozillademos.org/files/6457/mdn_logo_only_color.png" width="200" height="200" x="500" y="215" /> </svg> </body> </html> </code></pre> <p>こんな感じ。</p> <p><a href="https://crieit.now.sh/upload_images/a6f51a0f42f98ba2aa818a0116e59f605e2a8db81f639.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a6f51a0f42f98ba2aa818a0116e59f605e2a8db81f639.png?mw=700" alt="スクリーンショット 2020-01-23 17.48.11.png" /></a></p> <p>ハマったポイントにも書いたとおり、このままCanvasに書き出すと、<br /> 保存時に画像が表示されないので、動的にDataURLを取得してセットするといい感じ。</p> <pre><code class="html"><html> <body> <svg viewbox="0 0 1200 630" width="1200" height="630" style="background-color: lightgray;" > <image xlink:href="data:image/svg+xml;charset=utf-8;base64,...略" width="200" height="200" x="500" y="215" /> </svg> </body> </html> </code></pre> <p>URLからDataURLに変換するのは、こんな感じ。</p> <pre><code class="typescript">// svg2DataURL.ts /** * URLをDataURLに変換 * @param 変換したい画像のURL */ export default function url2DataURL( url: string ): Promise<HTMLCanvasElement> { return new Promise((resolve, reject) => { // 1. Canvasを用意する const canvas = document.createElement("canvas"); canvas.width = 1200; canvas.height = 630; const ctx = canvas.getContext("2d"); if (!ctx) { reject(Error("Create Canvas Error...")); return; } // 2. SVGを読み込む<image>を用意する const image = new Image(); image.decoding = "async"; image.onload = () => { // 4. 読み込みが完了したら、Canvasに書き出して、 ctx.drawImage(image, 0, 0, 1200, 630); // 5. CanvasからDataURLを取得する resolve(canvas.toDataURL()); }; image.onerror = e => reject(e); // 3. 作成した<image>にURLでセットして、読み込み開始 image.src = url; }); } </code></pre> <h3 id="画面サイズに合うように表示する(resize対応)"><a href="#%E7%94%BB%E9%9D%A2%E3%82%B5%E3%82%A4%E3%82%BA%E3%81%AB%E5%90%88%E3%81%86%E3%82%88%E3%81%86%E3%81%AB%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%28resize%E5%AF%BE%E5%BF%9C%29">画面サイズに合うように表示する(resize対応)</a></h3> <p>OGP画像としていい感じのサイズで保存したいけど、<br /> 表示するときは画面サイズにあった感じにしたい。。</p> <p>svgにstyleが使えるので、リサイズ時にスケールを計算して、<br /> <code>transform: scale();</code>で縮小する感じにしてみた。</p> <pre><code class="html"><template> <div id="svg-wrapper" class="svg-wrapper"> <svg class="svg-content" :viewbox="`0 0 ${svgWidth} ${svgHeight}`" :width="svgWidth" :height="svgHeight" :style="style"> <rect x="550" y="265" width="100" height="100" fill="blue" /> <circle cx="550" cy="265" r="30" fill="none" stroke="red" stroke-width="5" /> </svg> </div> </template> <script lang="ts"> import { Component, Vue } from "nuxt-property-decorator"; @Component() export default class SvgPage extends Vue { private svgWidth: number = 1200; private svgHeight: number = 630; private scale: number = 1; mounted() { // マウント時にリサイズする this.$nextTick(() => this.handleResize()); // windowのresizeイベントのリスナーに登録して、 // 画面サイズが変わったら、スケールを再計算するようにする window.addEventListener("resize", this.handleResize); } beforeDestroy() { // 破棄されるときに、リスナーの登録を解除する window.removeEventListener("resize", this.handleResize); } // リサイズ用のスケールを計算する処理 private handleResize() { const elm = document.getElementById("svg-wrapper"); if (!elm) return; this.rect = elm.getBoundingClientRect(); this.scale = this.rect.width / this.svgWidth; } // **************************************************** // * computed // **************************************************** private get style() { // 計算したスケールで縮小するようにtransformを設定する return { transform: `scale(${this.scale})` }; } } </script> <style> svg { transform-origin: 0 0; } .svg-wrapper { position: relative; width: 100%; height: auto; } .svg-wrapper:before { content: ""; display: block; padding-top: 52.5%; /* 630 / 1200 x 100 */ } .svg-content { position: absolute; top: 0; left: 0; } </style> </code></pre> <p>ただ、このまま保存すると縮小されたままになるので、<br /> deepコピーでクローンして、transformをクリアしてから書き出すようにする。</p> <pre><code class="typescript">// svg2DataURL.ts /** * svgをpngに変換 * @param svgElement <svg>のHTML要素 */ export default function svg2DataURL( svgElement: HTMLElement ): Promise<HTMLCanvasElement> { return new Promise((resolve, reject) => { // deepコピーでクローン const elm = svgElement.cloneNode(true) as HTMLElement; // transformをクリア elm.style.transform = ""; // ... 略 // 3. <svg>を文字列に変換 // ※ transformを削除したelmでsvgの文字列を取得 const svgXml = new XMLSerializer().serializeToString(elm); // ... 略 }); } </code></pre> <p>以上!!</p> <h2 id="こんなのつくってます!!"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%81%AE%E3%81%A4%E3%81%8F%E3%81%A3%E3%81%A6%E3%81%BE%E3%81%99%21%21">こんなのつくってます!!</a></h2> <p>積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!<br /> <a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>は、Nuxt.js+Firebaseで開発してます!</p> <p><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/></p> <p>もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ</p> <p>要望・感想・アドバイスなどあれば、<br /> 公式アカウント(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/MemoryLoverz">@MemoryLoverz</a>)や開発者(<a target="_blank" rel="nofollow noopener" href="https://twitter.com/kira_puka">@kira_puka</a>)まで♪</p> <h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Element/image"> - SVG: Scalable Vector Graphics | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTML/CORS_enabled_image">画像とキャンバスをオリジン間で利用できるようにする - HTML: HyperText Markup Language | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Element/foreignObject"> - SVG: Scalable Vector Graphics | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://kamoqq.info/post/nodejs-image-processor-sharp/">Node.js向け画像編集ライブラリSharp - kamoqq.info</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://sharp.pixelplumbing.com/">sharp - High performance Node.js image processing</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://forum.vuejs.org/t/does-vue-2-0-break-svg-foreignobject/2566">Does vue 2.0 break SVG foreignObject? - Get Help - Vue Forum</a></li> <li><a target="_blank" rel="nofollow noopener" href="http://nakajmg.hatenablog.com/entry/2019/08/30/133330">Vueでsvgファイルをいい感じに扱う - じまろぐ</a></li> <li><a target="_blank" rel="nofollow noopener" href="http://cocu.hatenablog.com/entry/2014/01/14/214917">svgにhtmlを組み込んで、テキストを折り返したりcanvasを使ったり - cocuh's note</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://inside.pixiv.blog/subal/7123">文字レイヤーを支える技術 - pixiv inside</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yuukive/items/ede7c087843d2f7ef979">VueコンポーネントでWindowサイズ変更検知&値取得 - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/haribote/items/b17d46b9679ce2fb2712">一発芸!SVGでHTMLを画像化する - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/SVG_Fonts">SVG Fonts - SVG: Scalable Vector Graphics | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://blanche-toile.com/web/css-font-face">CSSの@font-faceでGoogle Fontsのwebフォントを利用する方法 | Free Style</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/Window/onresize">window.onresize - Web API | MDN</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ryounagaoka/items/a98f59347ed758743b8d">CSSだけでアスペクト比を固定するテク - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6">Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/you8/items/bc5cbe887101863b242b">画像生成してOGPに設定する - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://creive.me/archives/17653/#word-breakoverflow-wrap">【CSS】 テキストを折り返す方法!自動で改行・レスポンシブにも対応 | creive【クリーブ】</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/15487 2019-10-17T05:05:57+09:00 2019-10-17T05:05:57+09:00 https://crieit.net/posts/fly-io-SPA-OGP-package-json fly.ioでSPA&OGP芸を最速かつゼロ依存でやる(package.jsonすら不要) <p>個人開発界隈ではOGP芸というものが流行っているみたいですね。</p> <p>SNSにシェアする際に表示されるOGPの設定や画像をいい感じにするみたいなことのことですが、SPAではこれを生成するのが割と面倒だったりします。</p> <p>自分が作っている<a target="_blank" rel="nofollow noopener" href="https://www.g-g-g-g.games">g4</a>でもOGPを使ったシェアを実装していますが、SSRを使って配信していて、割と設定が面倒だったりしています。</p> <p>こんなの<br /> <a href="https://crieit.now.sh/upload_images/0f6a4a0608c5dc3550a5c44abd805d7e5da777f12f09e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0f6a4a0608c5dc3550a5c44abd805d7e5da777f12f09e.png?mw=700" alt="68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f35313430322f63356465616336612d333663642d366131342d646434302d3139333036306636353732612e706e67.png" /></a></p> <p>今回、なるべくSSRをしないでSPAのままやってみることはできないかというのをfly.ioで試してみました。</p> <h1 id="作ったものの概要"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%E3%81%AE%E6%A6%82%E8%A6%81">作ったものの概要</a></h1> <ul> <li>実際にOGP画像として表示される画像は<code>/image.png?i=0</code>で配信する。</li> <li><code>/</code>に<code>?i=0</code>のようなパラメータをアクセスすると、入力した↑を含むOGPが反映されたhtmlを表示する</li> <li>fly.ioが日本語に対応してないので、titleに日本語は使えない</li> <li>せっかくfly.ioを使ってますが、今回はOGPだけにフォーカスしたいのでキャッシュ周りは実装してません</li> </ul> <h1 id="作ったもの"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">作ったもの</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://og-image-sample.edgeapp.net?i=0">https://og-image-sample.edgeapp.net?i=0</a></p> <p>or</p> <p><a target="_blank" rel="nofollow noopener" href="https://og-image-sample.edgeapp.net?i=1">https://og-image-sample.edgeapp.net?i=1</a></p> <p>これをtwitterやfacebookなどでシェアしてみてください。</p> <h1 id="作り方"><a href="#%E4%BD%9C%E3%82%8A%E6%96%B9">作り方</a></h1> <p>事前にflyのcliをPCにインストールしておく<br /> npmは必要ないです(あったほうが便利だけど最速なので)</p> <p>必要なファイルは4つだけ<br /> - index.js: なんか処理書くやつ<br /> - template.html: テンプレートです。ここに情報を流し込む<br /> - fly.yml: なんか設定書くやつ<br /> - sample.svg: OGP芸したい適当なsvg。今回はURLによって内容を書き換えるため雑に<span>{</span><span>{</span>title<span>}</span><span>}</span>みたいなやつをここに書いて、テキスト置換でそこに内容を埋め込んでます。</p> <p>``` html:template.html<br /> <!DOCTYPE html><br /> <html lang="ja"><br /> <head><br /> <meta charset="utf-8" /><br /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /><br /> <meta name="theme-color" content="#000000" /><br /> <title>fly.ioで最速ogp芸</title><br /> <meta name="description" content="by shwld" /><br /> <meta name="twitter:card" content="summary_large_image" /><br /> <meta name="twitter:creator" content="@shwld" /><br /> <meta property="og:type" content="website" /><br /> <meta property="og:url" content="https://www.g-g-g-g.games" /><br /> <meta property="og:title" content="shwld" /><br /> <meta property="og:description" content="shwld" /><br /> <meta property="og:image" content="https://www.g-g-g-g.games/assets/image.png" /><br /> <meta property="og:image:alt" content="shwld" /><br /> </head><br /> <body><br /> <h1 id="fly.ioでogp芸をする場合のサンプルだよ"><a href="#fly.io%E3%81%A7ogp%E8%8A%B8%E3%82%92%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88%E3%81%AE%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E3%81%A0%E3%82%88">fly.ioでogp芸をする場合のサンプルだよ</a></h1><br /> </body><br /> </html></p> <pre><code><br />templateはこんな感じになりました。 ogpのcontentは置き換えるので何でもいいです。とりあえずさり気なくg4のものを入れてアピールしておきます。 次に`index.js`です。こちらにすべての処理を書きます。 ``` js:index.js import { Image } from '@fly/image' import { mount } from "@fly/fetch/mount" // templateをテキストで取得するFunction async function getTemplate() { const resp = await fetch("file://src/template.html") return await resp.text() } // svgをテキストで取得しつつ中身ちょっと埋め込めるFunction async function getSvgText(title) { const resp = await fetch("file://src/sample.svg") const text = await resp.text() // g4(https://www.g-g-g-g.games)ではReactでテキスト化されたsvgを吐いてるが、とりあえず単純に置換する return text.replace('<span>{</span><span>{</span>content<span>}</span><span>}</span>', title) } // 画像形式のレスポンス作るFunction async function responseImage(svgText) { const svgResp = new Response(Buffer.from(svgText)) const buf = await svgResp.arrayBuffer() const png = new Image(buf).png() const result = await png.toBuffer() return new Response(result.data, { headers: { 'Content-Type': 'image/png', 'Content-Length': result.data.byteLength.toString(), } }) } // 出力データのパターン const TITLES = [ 'g4 is pomodoro rpg!', 'fly.io de OGP!!', ] // fly.ioのrouterみたいなやつ。ここに処理を書いてく。 const mounts = mount({ // このパスでogpの画像を生成する '/image.png': async (req, init) => { const url = new URL(req.url) // URLからQueryStringを取得 const index = url.searchParams.get('i') if (index !== '0' && index !== '1') { return new Response('not found', { status: 404 }) } // 対応したタイトルを取得 const title = TITLES[index] // タイトルをsvgに埋め込んだテキストを作る const svgText = await getSvgText(title) // svgからpng画像を生成する return responseImage(svgText) }, // このパスをシェアする '/': async (req, init) => { const url = new URL(req.url) // URLからQueryStringを取得 const index = url.searchParams.get('i') if (index !== '0' && index !== '1') { return new Response('not found', { status: 404 }) } // 対応したタイトルを取得 const title = TITLES[index] // テンプレートをparseして編集できるようにする const doc = Document.parse(await getTemplate()) // テンプレートにOGPを埋め込む doc.querySelector('meta[name="description"]').setAttribute('content', title) doc.querySelector('meta[property="og:url"]').setAttribute('content', url.href) doc.querySelector('meta[property="og:title"]').setAttribute('content', title) doc.querySelector('meta[property="og:description"]').setAttribute('content', title) doc.querySelector('meta[property="og:image"]').setAttribute('content', `${url.origin}/image.png?i=${index}`) doc.querySelector('meta[property="og:image:alt"]').setAttribute('content', title) // htmlを返す return new Response(doc.documentElement.outerHTML, { headers: { 'Content-Type': 'text/html' }, status: 200, }) }, }) // リクエストをmountsの定義を使って処理するように設定する fly.http.respondWith(mounts) </code></pre> <p>index.jsはこんな感じ。<br /> <code>/</code> は<code>template.html</code> のogpを書き換えて出すだけ。<br /> <code>/image</code> はsvgファイルを読み込んで文字列を置換したものをpngに変換して返してます。</p> <p>fly.ymlやsvgファイルは特に特別な設定はないのでドキュメントや最後にソースを貼るのでそちらを見ていただければ。</p> <p>あとは、fly.ioのコンソールでアプリを作って、<code>fly deploy</code> するだけ。</p> <p>再度になりますが、</p> <p><a target="_blank" rel="nofollow noopener" href="https://og-image-sample.edgeapp.net?i=0">https://og-image-sample.edgeapp.net?i=0</a></p> <p>or</p> <p><a target="_blank" rel="nofollow noopener" href="https://og-image-sample.edgeapp.net?i=1">https://og-image-sample.edgeapp.net?i=1</a></p> <p>これをtwitterやfacebookなどでシェアしてみてください。</p> <p>以上になります。ソースは<a target="_blank" rel="nofollow noopener" href="https://github.com/shwld/fly.io-og-image">こちら</a>にあります。</p> shwld tag:crieit.net,2005:PublicArticle/15330 2019-08-16T23:11:24+09:00 2019-08-17T07:36:18+09:00 https://crieit.net/posts/Laravel-OPG LaravelでOGPを作る <p>TwitterにURLをシェアした時に表示される画像であるOGPは今やWebサービスを作る時に当たり前のものになってきました。しかもWebサービスの場合はだいたい投稿内容に応じて動的に生成を行うため、プログラムによる自動生成が必要となります。</p> <p>各サービスで色々な作り方をされています。例えばPuppeteerというChromeを用いたライブラリを使用したり、ImageMagickなどのライブラリや、JavaScriptのcanvasを使う方法などです。</p> <p>Laravel、というか素のPHPにもGDという標準ライブラリがあり、そちらでも簡単に作成することが出来ますのでその方法です。ちなみにこういうOGPを作成します。</p> <p><a href="https://crieit.now.sh/upload_images/271ea78b750b6f8de3da8215ea2430415d572fbe4d057.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/271ea78b750b6f8de3da8215ea2430415d572fbe4d057.png?mw=700" alt="" /></a></p> <h2 id="要件"><a href="#%E8%A6%81%E4%BB%B6">要件</a></h2> <p>サーバーでGDが有効になっている必要があります。また、pngやjpeg等の必要なライブラリも有効になっている必要があり、且つ今回は文字を入れるのでfreetypeも有効になっている必要があります。phpinfoで各サポートが有効になっているかを確認してください。</p> <h2 id="素材を準備"><a href="#%E7%B4%A0%E6%9D%90%E3%82%92%E6%BA%96%E5%82%99">素材を準備</a></h2> <p>今回はそれっぽく見せるために最初に背景画像を描画します。そのためまず<a target="_blank" rel="nofollow noopener" href="https://www.pakutaso.com/">ぱくたそ</a>から適当な背景画像をお借りしました。また、フォントも必要ですので<a target="_blank" rel="nofollow noopener" href="http://modi.jpn.org/font_mushin.php">無心</a>をお借りしています。</p> <h2 id="OGPの作り方"><a href="#OGP%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9">OGPの作り方</a></h2> <p>コードをそのまま載っけます。普通にPHPで画像を作っているだけです。例えば今回はPostControllerに追記します。</p> <pre><code class="php"> public function ogp(Post $post) { // OGPのサイズ $w = 600; $h = 315; // 1行の文字数 $partLength = 10; $fontSize = 30; $fontPath = resource_path('font/mushin.otf'); // 画像を作成 $image = \imagecreatetruecolor($w, $h); // 背景画像を描画 $bg = \imagecreatefromjpeg(resource_path('image/HIRO95_yuubaenokage_TP_V4.jpg')); imagecopyresampled($image, $bg, 0, 0, 0, 0, $w, $h, 800, 533); // 色を作成 $white = imagecolorallocate($image, 255, 255, 255); $grey = imagecolorallocate($image, 128, 128, 128); // 各行に分割 $parts = []; $length = mb_strlen($post->title); for ($start = 0; $start < $length; $start += $partLength) { $parts[] = mb_substr($post->title, $start, $partLength); } // テキストの影を描画 $this->drawParts($image, $parts, $w, $h, $fontSize, $fontPath, $grey, 3); // テキストを描画 $this->drawParts($image, $parts, $w, $h, $fontSize, $fontPath, $white); ob_start(); imagepng($image); $content = ob_get_clean(); // 画像としてレスポンスを返す return response($content) ->header('Content-Type', 'image/png'); } /** * 各行の描画メソッド */ private function drawParts($image, $parts, $w, $h, $fontSize, $fontPath, $color, $offset = 0) { foreach ($parts as $i => $part) { // サイズを計算 $box = \imagettfbbox($fontSize, 0, $fontPath, $part); $boxWidth = $box[4] - $box[6]; $boxHeight = $box[1] - $box[7]; // 位置を計算 $x = ($w - $boxWidth) / 2; $y = $h / 2 + $boxHeight / 2 - $boxHeight * count($parts) * 0.5 + $boxHeight * $i; \imagettftext($image, $fontSize, 0, $x + $offset, $y + $offset, $color, $fontPath, $part); } } </code></pre> <h2 id="ルーティングを設定"><a href="#%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%92%E8%A8%AD%E5%AE%9A">ルーティングを設定</a></h2> <p>routes/web.phpに画像を表示するためのルーティングを設定します。これで <code>http://localhost:8000/posts/1/ogp.png</code> のようなURLでアクセスできるようになります。</p> <pre><code class="php">Route::get('posts/{id}/ogp.png', 'PostController@ogp'); </code></pre> <h2 id="メタタグを設定"><a href="#%E3%83%A1%E3%82%BF%E3%82%BF%E3%82%B0%E3%82%92%E8%A8%AD%E5%AE%9A">メタタグを設定</a></h2> <p>あとは詳細ページに下記のようなメタタグを追加しておけば詳細ページのURLをツイートするだけで画像が表示されるようになります。</p> <pre><code class="html">@section('additionalMeta') <meta property="og:image" content="<span>{</span><span>{</span> config('app.url') <span>}</span><span>}</span>/posts/<span>{</span><span>{</span> $post->id <span>}</span><span>}</span>/ogp.png"> <meta name="twitter:card" content="summary"> <meta name="twitter:image" content="<span>{</span><span>{</span> config('app.url') <span>}</span><span>}</span>/posts/<span>{</span><span>{</span> $post->id <span>}</span><span>}</span>/ogp.png"> @endsection </code></pre> <p>(↑この例だと実際には共通レイアウトに <code>@yield('addtionalMeta')</code> を追記する必要があります)</p> <h2 id="確認"><a href="#%E7%A2%BA%E8%AA%8D">確認</a></h2> <p>Twitterの<a target="_blank" rel="nofollow noopener" href="https://cards-dev.twitter.com/validator">Card validator</a>にそのURLを入力すれば正しく表示されるかテストが出来ます。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>全部プログラムで描画しているため改造する時のメンテナンス等は大変かもしれませんが、とりあえずぱぱっと作っておきたいときなどは非常に便利です。 Have a happy OGP life!</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15324 2019-08-15T13:00:18+09:00 2019-08-15T13:02:33+09:00 https://crieit.net/posts/Nuxt-SPA-Firebase-SEO-OGP-head Nuxt(SPA)+FirebaseでSEO!OGP!: 特定のパスだけheadだけ返すやつ <p>最近つくった<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>をNuxtのSPAで作成しているけど、<br /> シェアされたときにいい感じに画像とかを表示してほしいのでやってみた。</p> <p>N番煎じ感がつよいけれど、自分の整理用〜</p> <h3 id="全体の流れ"><a href="#%E5%85%A8%E4%BD%93%E3%81%AE%E6%B5%81%E3%82%8C">全体の流れ</a></h3> <ol> <li>該当のURLにアクセスがあったらリライトでFunctionを呼び出す(Hostingのrewrite)</li> <li>FunctionでヘッダだけのHTMLを生成。ボディには仮のパスへリダイレクト(Function)</li> <li>HTMLのリダイレクト先をさらにNuxt側で正しいパスリダイレクト(nuxt.config.ts)</li> </ol> <p>若干複雑...</p> <p>図的にはこんな感じ</p> <p><a href="https://crieit.now.sh/upload_images/aaf2c8966686283fb82e6e2b3acfa8685d54d85d642f9.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/aaf2c8966686283fb82e6e2b3acfa8685d54d85d642f9.png?mw=700" alt="スクリーンショット 2019-08-07 12.08.48.png" /></a></p> <h4 id="Function側のコード(index.js)"><a href="#Function%E5%81%B4%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%28index.js%29">Function側のコード(index.js)</a></h4> <p>まずは、Cloud Function for Firebaseから。<br /> リスエストのパスに応じてDBの値を取得して、OPG用のHTMLを生成。</p> <pre><code class="typescript">const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); const db = admin.firestore(); // ******************************************************** // * Generate OPG HEAD // ******************************************************** /** * OGP用のヘッダだけのHTMLを返す関数 * @param {String} TITLE タイトル * @param {String} DESCRIPTION ディスクリプション * @param {String} OGP_URL OGP画像のURL * @param {String} PAGE_URL 該当ページのURL * @param {String} REDIRECT_URL リダイレクト先のURL */ const createHtml = (TITLE, DESCRIPTION, OGP_URL, PAGE_URL, REDIRECT_URL) => { return `<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>${TITLE}</title> <meta property="og:title" content="${TITLE}"> <meta property="og:image" content="${OGP_URL}"> <meta property="og:description" content="${DESCRIPTION}"> <meta property="og:url" content="${PAGE_URL}"> <meta property="og:type" content="article"> <meta property="og:site_name" content="${SITE_NAME}"> <meta name="twitter:site" content="${BASE_URL}"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:title" content="${TITLE}"> <meta name="twitter:image" content="${OGP_URL}"> <meta name="twitter:description" content="${DESCRIPTION}"> </head> <body> <script type="text/javascript">window.location="${REDIRECT_URL}";</script> </body> </html> `; }; /** * '/user/<userId>'に対応するHTMLを返すFunction */ const BASE_URL = '<サイトのBASE_URL>' exports.users = functions.https.onRequest(async (req, res) => { try { // PATHからパスパラメータを取得 const [, , userId] = req.path.split("/"); if (!userId) throw new Error(`userId is empty`); // パスパラメータを使って、DBからデータを取得 const docRef = db.collection("users").doc(userId); const snap = await docRef.get(); if (!snap.exists) throw new Error(`Not Found: userId=${userId}`); // DBのデータからHTML作成に必要なデータを用意 const user = snap.data(); const title = `${user.name}さんのページ`; const desc = `${user.name}さんのページの詳細です`; const ogpURL = "<該当ユーザのOGP画像のURL>"; const pageURL = `${BASE_URL}/user/${userId}`; const redirectURL = `/_user/${userId}`; // ヘッダだけのHTMLを生成 const html = createHtml(title, desc, ogpURL, pageURL, redirectURL); // キャッシュを設定 res.set("Cache-Control", "public, max-age=600, s-maxage=600"); // 生成したHTMLを返却 res.status(200).end(html); } catch (err) { // エラーが発生したら'/'にリダイレクト console.warn(err); res.redirect("/"); } }); </code></pre> <h4 id="firebase.jsonの設定"><a href="#firebase.json%E3%81%AE%E8%A8%AD%E5%AE%9A">firebase.jsonの設定</a></h4> <p>firebase.jsonの設定。該当のパスにアクセスされたら、<br /> リライトでFunctionを呼び出すように設定を追加。</p> <pre><code class="json">{ "functions": { "source": "functions" }, "hosting": { "rewrites": [ // '/user/<userId>'へのアクセスがあったらFunctionsのusersを呼び出す { "source": "/user/*", "function": "users" }, { "source": "**", "destination": "/404.html" } ], }, } </code></pre> <h4 id="nuxt.config.tsの設定"><a href="#nuxt.config.ts%E3%81%AE%E8%A8%AD%E5%AE%9A">nuxt.config.tsの設定</a></h4> <p>nuxt.config.jsのrouterの設定。HTML内でリダイレクトされる先を、<br /> さらにrouter側でリダイレクト。もとに戻す感じに。</p> <pre><code class="typescript">const config: NuxtConfiguration = { /* ** Router configuration */ router: { extendRoutes(routes: NuxtRouteConfig[], resolve) { routes.push({ path: "/_user/:uid", redirect: "/user/:uid", chunkNames: {} }); } }, } </code></pre> <h4 id="注意!! 動的パラメタのあるパスだけ使えます"><a href="#%E6%B3%A8%E6%84%8F%21%21+%E5%8B%95%E7%9A%84%E3%83%91%E3%83%A9%E3%83%A1%E3%82%BF%E3%81%AE%E3%81%82%E3%82%8B%E3%83%91%E3%82%B9%E3%81%A0%E3%81%91%E4%BD%BF%E3%81%88%E3%81%BE%E3%81%99">注意!! 動的パラメタのあるパスだけ使えます</a></h4> <p>ちなみに、該当のパスにHostingのHTMLがあるとダメ...<br /> <a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/hosting/url-redirects-rewrites?hl=ja#section-priorities">Hostingの優先度</a>がこんな感じ...</p> <ol> <li>予約済み名前空間(/__*)</li> <li>リダイレクトの構成</li> <li>正確に一致する静的コンテンツ</li> <li>リライトの構成</li> </ol> <p>リライトよりも静的コンテンツのほうが優先度が高いので、<br /> リライトでFunctionを呼び出されるよりも先にHTMLが返されてしまう...</p> <p><code>nuxt generate</code>するとHTMLが配置されてしまうので、<br /> 動的パラメタじゃないとダメかも...</p> <p>以上!!</p> <h2 id="【PR】積読ハウマッチをリリースしました!"><a href="#%E3%80%90PR%E3%80%91%E7%A9%8D%E8%AA%AD%E3%83%8F%E3%82%A6%E3%83%9E%E3%83%83%E3%83%81%E3%82%92%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%81%BE%E3%81%97%E3%81%9F%EF%BC%81">【PR】積読ハウマッチをリリースしました!</a></h2> <p>積んでいる本の総額がわかる読書管理サービス<br /> 『<a target="_blank" rel="nofollow noopener" href="https://tsundoku.site">積読ハウマッチ</a>』をリリースしました♪</p> <p><a href="https://crieit.now.sh/upload_images/f86129c7aa3dee1104223eaa7d236fb55d54d936f0f8e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f86129c7aa3dee1104223eaa7d236fb55d54d936f0f8e.png?mw=700" width="25%"/></a></p> <p>積読が多い方も、少ない方も、ない方も、<br /> ぜひお試しください(´ω`)</p> <h1 id="参考にしたサイト様"><a href="#%E5%8F%82%E8%80%83%E3%81%AB%E3%81%97%E3%81%9F%E3%82%B5%E3%82%A4%E3%83%88%E6%A7%98">参考にしたサイト様</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/yuneco/items/5e526464939082862f5d">SNS映えするWebアプリを...!FirebaseとVue.jsでSPAのOGP画像の動的生成をやってみたら案外楽だった - Qiita</a></li> </ul> きらぷか@積読ハウマッチ/SSSAPIなど tag:crieit.net,2005:PublicArticle/14891 2019-03-29T23:00:19+09:00 2019-03-29T23:00:19+09:00 https://crieit.net/posts/OGP-Twitter-LINE-8 OGP取得用クローラのユーザーエージェントを、TwitterやLINEなど8サービスで調べてみた <p>SNSなどで、投稿されたWebサイトのリンクがリッチに表示される「OGP (Open Graph Protocol)」機能。<br /> Webサイトに簡単なメタタグを組み込むだけでOGP表示されるため、対応するサイトは増えています。<br /> しかし、OGP用のメタタグは一般ユーザーからは見えないし、サムネイル動的生成などでサーバーに大きな負荷がかかることもあるので、できればOGP取得用クローラ以外にはメタタグを生成しないようにしたいもの。<br /> そこで、UA(ユーザーエージェント)からクローラによるアクセスか判定するために、実際にOGP取得用クローラのUAを記録して調べてみました。</p> <h4 id="結果"><a href="#%E7%B5%90%E6%9E%9C">結果</a></h4> <p>2019年3月28日に記録された文字列をそのまま掲載しています。</p> <div class="table-responsive"><table> <thead> <tr> <th>サービス</th> <th>User Agent</th> </tr> </thead> <tbody> <tr> <td>Twitter</td> <td>Twitterbot/1.0</td> </tr> <tr> <td>Facebook</td> <td>facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)</td> </tr> <tr> <td>LINE</td> <td>facebookexternalhit/1.1;line-poker/1.0</td> </tr> <tr> <td>メッセージ(iOS)</td> <td>Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.4 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.4 facebookexternalhit/1.1 Facebot Twitterbot/1.0</td> </tr> <tr> <td>Discord</td> <td>Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)</td> </tr> <tr> <td>Skype</td> <td>Mozilla/5.0 (Windows NT 6.1; WOW64) SkypeUriPreview Preview/0.5</td> </tr> <tr> <td>Slack</td> <td>Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)</td> </tr> <tr> <td>Plurk</td> <td>Mozilla/5.0 (compatible; PlurkBot/1.0; +https://www.plurk.com/) Firefox/61.0</td> </tr> </tbody> </table></div> <p>もし他にもOGP表示に対応しているサービスをご存知でしたら、<a target="_blank" rel="nofollow noopener" href="https://twitter.com/barley_ural">@barley_ural</a>までご連絡いただければ調査して追記していきます。</p> <p>おわり</p> ウラル