tag:crieit.net,2005:https://crieit.net/tags/Vercel/feed 「Vercel」の記事 - Crieit Crieitでタグ「Vercel」に投稿された最近の記事 2021-11-05T21:44:25+09:00 https://crieit.net/tags/Vercel/feed tag:crieit.net,2005:PublicArticle/17741 2021-11-05T21:44:25+09:00 2021-11-05T21:44:25+09:00 https://crieit.net/posts/microcms-api-update-gatsby-update-20211106 microCMS の X-API-KEY リニューアルに合わせて Vercel の設定と Gatsby のプロジェクトを更新する <p>メール通知で microCMS の X-API-KEY がリニューアルされたという通知を受け取って対応することにしました。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://blog.microcms.io/renew-x-microcms-api-key/">APIキーをX-MICROCMS-API-KEYへとリニューアルしました。 | microCMSブログ</a></li> </ul> <blockquote> <p>従来のX-API-KEY, X-WRITE-API-KEY, X-DRAFT-KEYは統合され、X-MICROCMS-API-KEYに変更となります。</p> <p><a target="_blank" rel="nofollow noopener" href="https://blog.microcms.io/renew-x-microcms-api-key/">APIキーをX-MICROCMS-API-KEYへとリニューアルしました。 | microCMSブログ</a></p> </blockquote> <p>とのことなので、これに対応することにします。</p> <h2 id="microCMS"><a href="#microCMS">microCMS</a></h2> <p>まずは microCMS 側の作業。新しいAPIキーを発行し直します。</p> <p><a href="https://crieit.now.sh/upload_images/b639bdef8b9834d65ed80dcb79d999e46185263f658dd.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b639bdef8b9834d65ed80dcb79d999e46185263f658dd.jpg?mw=700" alt="APIキーの一覧" /></a></p> <p>「APIキー管理」のAPIキーの一覧で「旧X-API-KEY」があることを確認。</p> <p><a href="https://crieit.now.sh/upload_images/6adc5584ea41a4d7620ea61147890b1261852649a9f13.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6adc5584ea41a4d7620ea61147890b1261852649a9f13.jpg?mw=700" alt="APIキーの詳細" /></a></p> <p>選択して詳細画面から「削除」を選択。</p> <p>続いて新しいAPIキーを発行し直します。</p> <p><a href="https://crieit.now.sh/upload_images/eef5b0f7a42cda381863a3eae0f6894361852651849ef.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/eef5b0f7a42cda381863a3eae0f6894361852651849ef.jpg?mw=700" alt="新X-MICROCMS-API-KEYの発行" /></a></p> <p>X-MICROCMS-API-KEY を発行します。今回はAPI経由では参照しかしないので GET のみの権限とします。</p> <p><a href="https://crieit.now.sh/upload_images/8febcc76cb4c74b656c4720ed16f44a06185265a7d678.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8febcc76cb4c74b656c4720ed16f44a06185265a7d678.jpg?mw=700" alt="APIキーをコピー" /></a></p> <p>発行されたAPIキーをコピーして控えます。</p> <p>これで microCMS 側の作業は完了です。</p> <h2 id="Vercel"><a href="#Vercel">Vercel</a></h2> <p>続いて Vercel にログインします。</p> <p><a href="https://crieit.now.sh/upload_images/1ae9a58b4f0e8ee01d8a591c1629ff9b618526623540c.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1ae9a58b4f0e8ee01d8a591c1629ff9b618526623540c.jpg?mw=700" alt="Vercel の環境変数の一覧" /></a></p> <p>プロジェクトの「Settings」から「Enviroment Variables」に進みます。</p> <p><a href="https://crieit.now.sh/upload_images/3a0751306ac22bdcc1333827ce44d7f16185266ada888.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3a0751306ac22bdcc1333827ce44d7f16185266ada888.jpg?mw=700" alt="API_KEYの変更" /></a></p> <p><code>API_KEY</code>の値を先ほど控えた値に変更します。</p> <p>これでOKかと思ったのですが……。</p> <pre><code class="bash">00:03:22.489 error microCMS API ERROR: 00:03:22.489 statusCode: 401 00:03:22.489 message: X-MICROCMS-API-KEY header is invalid. 00:03:22.492 not finished source and transform nodes - 1.046s 00:03:23.631 Error: Command "npm run build" exited with 1 </code></pre> <p>ビルドログを見ると 401エラー でコケていますね。認証に失敗しているようです。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://vanilla-note.com/posts/gatsby4-1-microcms-3/">microCMSの新しいX-MICROCMS-API-KEYに対応する | Vanilla note</a></li> </ul> <p>検索すると、 Gatsby の microCMS 連携のパッケージも更新しないといけないとのこと。</p> <h2 id="Gatsby プロジェクト"><a href="#Gatsby+%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88">Gatsby プロジェクト</a></h2> <p>そこで該当パッケージのバージョンをアップデートします。</p> <pre><code class="json"> "dependencies": { // 略 "gatsby-source-microcms": "^1.2.1", // 略 } </code></pre> <p><code>gatsby-source-microcms</code> を</p> <pre><code class="json"> "dependencies": { // 略 "gatsby-source-microcms": "^2.0.0", // 略 } </code></pre> <p><code>2.0.0</code> にアップデート。</p> <p>これでビルドを走らせ、正常に完了することを確認しました。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://blog.microcms.io/renew-x-microcms-api-key/">APIキーをX-MICROCMS-API-KEYへとリニューアルしました。 | microCMSブログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://vanilla-note.com/posts/gatsby4-1-microcms-3/">microCMSの新しいX-MICROCMS-API-KEYに対応する | Vanilla note</a></li> </ul> <p>Vercel に関しては自分の過去の記事も参考にして辿りつつ……。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://labor.ewigleere.net/2021/08/29/gatsby-vercel-microcms-cooprate-2/">Gatsby.js + Vercel + microCMS の JAMStack 環境を構築する – Ewig Leere(Lab2)</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/17625 2021-08-29T20:31:13+09:00 2021-08-29T20:33:47+09:00 https://crieit.net/posts/vercel-setting-subdmain-20210829 Vercel のサイトに独自ドメインのサブドメインを割り当てる <p><a href="https://crieit.net/posts/gatsby-vercel-microcms-cooprate-20210829-2">Gatsby.js + Vercel + microCMS の JAMStack 環境のサイト</a>の仕上げとして、独自ドメインのサブドメインを割り当てます。</p> <h2 id="Vercel の設定"><a href="#Vercel+%E3%81%AE%E8%A8%AD%E5%AE%9A">Vercel の設定</a></h2> <p><a href="https://crieit.now.sh/upload_images/6c8fefbc8da18037c3d877db75358c4f612b6f437ef97.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6c8fefbc8da18037c3d877db75358c4f612b6f437ef97.jpg?mw=700" alt="Vercel のプロジェクトで「Settings」→「Domains」" /></a></p> <p>Vercel のプロジェクトで「Settings」→「Domains」と進みます。</p> <p>ここで割り当てたいドメインを入力して「Add」。</p> <p><a href="https://crieit.now.sh/upload_images/ca03daca645df58ed6024033974b91a2612b6f5218cf6.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ca03daca645df58ed6024033974b91a2612b6f5218cf6.jpg?mw=700" alt="Invalid Configuration" /></a></p> <p>いったん Invalid Configuration と怒られますがひとまず保留。</p> <h2 id="ドメイン管理側サービスの設定"><a href="#%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E7%AE%A1%E7%90%86%E5%81%B4%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E8%A8%AD%E5%AE%9A">ドメイン管理側サービスの設定</a></h2> <p>詳細部に書かれている <code>cname.vercel-dns.com</code> を CNAMEレコード として設定します。</p> <p><a href="https://crieit.now.sh/upload_images/074794efb552d4b7a428f7507a00f060612b6f5be2f85.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/074794efb552d4b7a428f7507a00f060612b6f5be2f85.jpg?mw=700" alt="ドメイン管理側のサービスで指示通りの CNAMEレコード を設定" /></a></p> <p>ドメイン管理側のサービスで指示通りの CNAMEレコード を設定します。これで DNS権威サーバ の設定が書き変わります。</p> <h2 id="DNSキャッシュサーバ への設定反映を確認"><a href="#DNS%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%82%B5%E3%83%BC%E3%83%90+%E3%81%B8%E3%81%AE%E8%A8%AD%E5%AE%9A%E5%8F%8D%E6%98%A0%E3%82%92%E7%A2%BA%E8%AA%8D">DNSキャッシュサーバ への設定反映を確認</a></h2> <p>これで30分くらい待ち、 DNSキャッシュサーバ 権威サーバの設定が反映されるのを確かめます。</p> <pre><code class="bash">> nslookup ## 略 > server 8.8.8.8 ## 略 > hogehoge.exmaple.jp サーバー: dns.google Address: 8.8.8.8 権限のない回答: 名前: hogehoge.exmaple.jp Address: 192.0.2.1 </code></pre> <p>まだですね……。</p> <pre><code class="bash">> hogehoge.exmaple.jp サーバー: dns.google Address: 8.8.8.8 権限のない回答: 名前: cname.vercel-dns.com Address: 76.76.21.21 Aliases: hogehoge.exmaple.jp </code></pre> <p>お、来ました。</p> <p><a href="https://crieit.now.sh/upload_images/c62ed7cbbbf12c4277c84f0a50ae0443612b6f654d637.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c62ed7cbbbf12c4277c84f0a50ae0443612b6f654d637.jpg?mw=700" alt="Vercel の設定画面もクリア" /></a></p> <p>Vercel の設定画面でもクリアになりました。</p> <p>これで指定したサブドメインにアクセスすると、確かにデフォルトの <code>vercel.app</code> と同じサイトにアクセスできることが確認できました。</p> <p>割り当て成功です。</p> <p>ちなみに、今回はドメイン管理側のサービスで <code>*.exmaple.jp IN A 192.0.2.1</code> という感じのサブドメインのワイルドカード指定の Aレコード が存在していたのでそれが邪魔しないか気がかりでしたが、大丈夫のようです。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://dev.classmethod.jp/articles/vercel-custom-domain-route53/">VercelでホスティングしているサイトにRoute53で取得したドメインをサブドメインとして設定する | DevelopersIO</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/takaken/items/530d19a549a730c15fcd">VercelのサイトにVercel以外のDNSで管理しているドメインのサブドメインを設定する - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/BlackMagician/items/48923a766f101425a677">Next.jsアプリをVercelで公開して、独自ドメインに反映する - Qiita</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/17623 2021-08-29T00:49:34+09:00 2021-08-29T00:49:34+09:00 https://crieit.net/posts/gatsby-vercel-microcms-cooprate-20210829-2 Gatsby.js + Vercel + microCMS の JAMStack 環境を構築する <p><a href="https://crieit.net/posts/gatsby-vercel-microcms-cooprate-20210829-1">microCMS の設定が完了した</a>ので、次は microCMS からデータを取得するように Gatsby.js の方に手を入れます。また、 microCMS と Vercel に設定を加えて連携させ、 Gatsby.js + Vercel + microCMS の JAMStack 環境を構築します。</p> <h2 id="改修"><a href="#%E6%94%B9%E4%BF%AE">改修</a></h2> <p>今回は<a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/starters/renyuanz/leonids">leonids: Gatsby Starter | Gatsby</a>を使用しているので、その前提で。</p> <p>テーマが変わったり、 microCMS のスキーマが変われば諸々の調整が必要となります。</p> <h3 id="パッケージの追加"><a href="#%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%81%AE%E8%BF%BD%E5%8A%A0">パッケージの追加</a></h3> <pre><code class="bash">> yarn add yarn add gatsby-source-microcms ## 略 Done in 231.79s. > yarn add marked ## 略 Done in 7.30s. </code></pre> <p>今回は microCMS を利用するので <code>gatsby-source-microcms</code> を追加します。また、本文コンテンツは Markdown で記述しているのですが Gatsby.js の自前の Markdownパーサ まで手が届かなかったので安直に <code>marked</code> を使いました。</p> <p>それから、内部的には <code>dotenv</code> も使用していますが <code>yarn.lock</code> を見たら既に入っていたので追加はしていません。</p> <h3 id="gatsby-config.js"><a href="#gatsby-config.js">gatsby-config.js</a></h3> <p>さて、改修の最初は Gatsby.js の設定から。</p> <pre><code class="javascript">module.exports = { // 略 plugins: [ { resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/content/blog`, name: `blog`, }, }, // 略 }, `gatsby-plugin-feed`, { // 略 ], // 略 } </code></pre> <p>これを以下のように修正。</p> <pre><code class="js">const activeEnv = process.env.GATSBY_ACTIVE_ENV || process.env.NODE_ENV if(activeEnv === "development") { require("dotenv").config({ path: `.env.${activeEnv}`, }) } module.exports = { // 略 plugins: [ { resolve: 'gatsby-source-microcms', options: { apiKey: process.env.API_KEY, serviceId: process.env.SERVICE_ID, apis: [{ endpoint: process.env.APIS_ENDPOINT, }], }, }, // 略 { resolve: `gatsby-plugin-feed`, options: { query: ` { site { siteMetadata { title description siteUrl } } } `, feeds: [ { serialize: ({ query: { site, allMicrocmsHogeHogeBlog } }) => { return allMicrocmsHogeHogeBlog.edges.map(edge => { return Object.assign({}, edge.node, { description: edge.node.keywords, date: edge.node.date, url: site.siteMetadata.siteUrl + edge.node.slug, guid: site.siteMetadata.siteUrl + edge.node.slug, custom_elements: [{ "content:encoded": edge.node.body }], }) }) }, query: ` { allMicrocmsHogeHogeBlog(sort: {fields: [date], order: DESC}) { totalCount pageInfo { perPage pageCount } edges { node { body createdAt date id keywords publishedAt revisedAt slug title updatedAt } } } } `, output: "/rss.xml", title: "HogeHoge Blog's RSS Feed", }, ], }, }, // 略 ], // 略 } </code></pre> <p>変更点は以下。</p> <ul> <li>ローカルのファイルをコンテンツのリソース参照先としていたのを microCMS をリソースとするように修正</li> <li><code>gatsby develop</code> 時は問題なかったのですが、 <code>gatsby build</code> 時に RSSフィードを生成する <code>gatsby-plugin-feed</code>プラグイン が通常の Markdown の構成をデフォルトにしており、 microCMS に置き換えたことでデータスキーマが異なるとエラーになってしまいました (本番ビルド時に気付いた)。 <ul> <li>そこで、 <code>gatsby-plugin-feed</code>プラグイン の GraphQL も今回定義した microCMS のスキーマに沿って変更しました。併せて <code>serialize</code>メソッド の中身もスキーマに合わせて調整。</li> </ul></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/plugins/gatsby-source-microcms/">gatsby-source-microcms | Gatsby</a>のサンプルコードでは Git 管理下になる <code>gatsby-config.js</code> に APIキー 等の情報がそのままベタ書きになってしまうので、 <code>dotenv</code> で外部ファイルに取り出すことで隠蔽しました <ul> <li>後述しますが、 Vercel の環境変数も <code>process.env</code> で渡ってくるので、<code>development</code> の場合は <code>dotenv</code> 経由の外部ファイルをパラメータのリソースに、 <code>production</code> の場合は Vercel の環境変数をリソースとするように書き分けました</li> </ul></li> </ul> <h3 id="gatby-node.js"><a href="#gatby-node.js">gatby-node.js</a></h3> <pre><code class="javascript">const path = require(`path`) const { createFilePath } = require(`gatsby-source-filesystem`) exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions const blogPost = path.resolve(`./src/templates/blog-post.js`) const result = await graphql( ` { allMarkdownRemark( sort: { fields: [frontmatter___date], order: DESC } limit: 1000 ) { edges { node { fields { slug } frontmatter { title } } } } } ` ) if (result.errors) { throw result.errors } // Create blog posts pages. const posts = result.data.allMarkdownRemark.edges posts.forEach((post, index) => { const previous = index === posts.length - 1 ? null : posts[index + 1].node const next = index === 0 ? null : posts[index - 1].node createPage({ path: post.node.fields.slug, component: blogPost, context: { slug: post.node.fields.slug, previous, next, }, }) }) // Create blog post list pages const postsPerPage = 5 const numPages = Math.ceil(posts.length / postsPerPage) Array.from({ length: numPages }).forEach((_, i) => { createPage({ path: i === 0 ? `/` : `/${i + 1}`, component: path.resolve("./src/templates/blog-list.tsx"), context: { limit: postsPerPage, skip: i * postsPerPage, numPages, currentPage: i + 1, }, }) }) } exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions if (node.internal.type === `MarkdownRemark`) { const value = createFilePath({ node, getNode }) createNodeField({ name: `slug`, node, value, }) } } </code></pre> <p>これを以下のように修正。</p> <pre><code class="javascript">const path = require(`path`) const { createFilePath } = require(`gatsby-source-filesystem`) exports.createPages = async ({ graphql, actions }) => { const { createPage } = actions const result = await graphql( ` { allMicrocmsHogeHogeBlog(sort: {fields: [date], order: DESC}) { totalCount pageInfo { perPage pageCount } edges { node { body createdAt date id keywords publishedAt revisedAt slug title updatedAt } } } } ` ) if (result.errors) { throw result.errors } // Create blog posts pages. const posts = result.data.allMicrocmsHogeHogeBlog.edges result.data.allMicrocmsHogeHogeBlog.edges.forEach((post, index) => { const previous = index === posts.length - 1 ? null : posts[index + 1].node const next = index === 0 ? null : posts[index - 1].node createPage({ path: post.node.slug, component: path.resolve('./src/templates/blog-post.js'), context: { slug: post.node.slug, previous, next, }, }); }); // Create blog post list pages const postsPerPage = result.data.allMicrocmsHogeHogeBlog.pageInfo.limit || 10 const numPages = Math.ceil(result.data.allMicrocmsHogeHogeBlog.totalCount / postsPerPage) console.log(result.data.allMicrocmsHogeHogeBlog.totalCount, postsPerPage) Array.from({ length: numPages }).forEach((_, i) => { createPage({ path: i === 0 ? `/` : `/${i + 1}`, component: path.resolve("./src/templates/blog-list.tsx"), context: { limit: postsPerPage, skip: i * postsPerPage, numPages, currentPage: i + 1, }, }) }) } exports.onCreateNode = ({ node, actions, getNode }) => { const { createNodeField } = actions if (node.internal.type === `microcmsHogeHogeBlog`) { const value = createFilePath({ node, getNode }) createNodeField({ name: `slug`, node, value, }) } } </code></pre> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/plugins/gatsby-source-microcms/">gatsby-source-microcms | Gatsby</a>のサンプルコードはシンプル過ぎて GraphQL初心者には分かりづらかったので、 <code>gatsby develop</code> で起動した <code>http://localhost:8000/___graphql</code> の GraphQL を試すGUIでひたすらAPIを叩いてパラメータを変えたりしてどういう GraphQL を作れば望んだレスポンスを得られるか確認しながら地道に調整していきました</li> <li>ベースのコードをなるべく変えないように調整して、各種プロパティの階層等を調整しました</li> <li><code>MarkdownRemark</code> や <code>allMarkdownRemark</code> が Markdownファイル をリソースとする場合のキーのような働きをしているのが分かったので、それを <code>http://localhost:8000/___graphql</code> で表示された microCMS のキーに変更したり</li> </ul> <p>GraphQL の構造やレスポンスに慣れるまで時間がかかりましたが、最終的には <code>http://localhost:8000/___graphql</code> であれこれ試したのが一番効果がありました。</p> <h3 id="blog-list.tsx"><a href="#blog-list.tsx">blog-list.tsx</a></h3> <p>記事一覧ページ。トップページもこのテンプレートから生成されています。</p> <pre><code class="javascript">// Gatsby supports TypeScript natively! import React from "react" import { PageProps, Link, graphql } from "gatsby" import Layout from "../components/layout" import SEO from "../components/seo" import { rhythm } from "../utils/typography" type PageContext = { currentPage: number numPages: number } type Data = { site: { siteMetadata: { title: string } } allMarkdownRemark: { edges: { node: { excerpt: string frontmatter: { title: string date: string description: string } fields: { slug: string } } }[] } } const BlogIndex = ({ data, location, pageContext, }: PageProps<Data, PageContext>) => { const siteTitle = data.site.siteMetadata.title const posts = data.allMarkdownRemark.edges const { currentPage, numPages } = pageContext const isFirst = currentPage === 1 const isLast = currentPage === numPages const prevPage = currentPage - 1 === 1 ? "/" : `/${currentPage - 1}` const nextPage = `/${currentPage + 1}` return ( <Layout location={location} title={siteTitle}> <SEO title="All posts" /> {posts.map(({ node }) => { const title = node.frontmatter.title || node.fields.slug return ( <article key={node.fields.slug}> <header> <h3 style=<span>{</span><span>{</span> marginBottom: rhythm(1 / 4), <span>}</span><span>}</span> > <Link style=<span>{</span><span>{</span> boxShadow: `none` <span>}</span><span>}</span> to={node.fields.slug}> {title} </Link> </h3> <small>{node.frontmatter.date}</small> </header> <section> <p dangerouslySetInnerHTML=<span>{</span><span>{</span> __html: node.frontmatter.description || node.excerpt, <span>}</span><span>}</span> /> </section> </article> ) })} <nav> <ul style=<span>{</span><span>{</span> display: `flex`, flexWrap: `wrap`, justifyContent: `space-between`, listStyle: `none`, padding: 0, <span>}</span><span>}</span> > <li> {!isFirst && ( <Link to={prevPage} rel="prev"> ← Previous Page </Link> )} </li> <li> {!isLast && ( <Link to={nextPage} rel="next"> Next Page → </Link> )} </li> </ul> </nav> </Layout> ) } export default BlogIndex export const pageQuery = graphql` query blogPageQuery($skip: Int!, $limit: Int!) { site { siteMetadata { title } } allMarkdownRemark( sort: { fields: [frontmatter___date], order: DESC } limit: $limit skip: $skip ) { edges { node { excerpt fields { slug } frontmatter { date(formatString: "MMMM DD, YYYY") title description } } } } } ` </code></pre> <p>これを以下のように改修。</p> <pre><code class="javascript">// Gatsby supports TypeScript natively! import React from "react" import { PageProps, Link, graphql } from "gatsby" import Layout from "../components/layout" import SEO from "../components/seo" import { rhythm } from "../utils/typography" type PageContext = { currentPage: number numPages: number } type Data = { site: { siteMetadata: { title: string } } allMicrocmsHogeHogeBlog: { edges: { node: { id: string keywords: string title: string updatedAt: string slug: string revisedAt: string publishedAt: string date: string createdAt: string body: string } }[] } } const BlogIndex = ({ data, location, pageContext, }: PageProps<Data, PageContext>) => { const siteTitle = data.site.siteMetadata.title const posts = data.allMicrocmsHogeHogeBlog.edges const { currentPage, numPages } = pageContext const isFirst = currentPage === 1 const isLast = currentPage === numPages const prevPage = currentPage - 1 === 1 ? "/" : `/${currentPage - 1}` const nextPage = `/${currentPage + 1}` return ( <Layout location={location} title={siteTitle}> <SEO title="All posts" /> {posts.map(({ node }) => { const title = node.title || node.slug return ( <article key={node.slug}> <header> <h3 style=<span>{</span><span>{</span> marginBottom: rhythm(1 / 4), <span>}</span><span>}</span> > <Link style=<span>{</span><span>{</span> boxShadow: `none` <span>}</span><span>}</span> to={node.slug}> {title} </Link> </h3> <small>{node.date}</small> </header> <section> <p dangerouslySetInnerHTML=<span>{</span><span>{</span> __html: node.keywords, <span>}</span><span>}</span> /> </section> </article> ) })} <nav> <ul style=<span>{</span><span>{</span> display: `flex`, flexWrap: `wrap`, justifyContent: `space-between`, listStyle: `none`, padding: 0, <span>}</span><span>}</span> > <li> {!isFirst && ( <Link to={prevPage} rel="prev"> ← Previous Page </Link> )} </li> <li> {!isLast && ( <Link to={nextPage} rel="next"> Next Page → </Link> )} </li> </ul> </nav> </Layout> ) } export default BlogIndex export const pageQuery = graphql` query blogPageQuery($skip: Int!, $limit: Int!) { site { siteMetadata { title } } allMicrocmsHogeHogeBlog( sort: {fields: [date], order: DESC} limit: $limit skip: $skip ) { edges { node { body createdAt date(formatString: "YYYY/MM/DD") id keywords publishedAt revisedAt slug title updatedAt } } } } ` </code></pre> <p>ここは大々的な改修というよりは、 GraphQL の変更と、それに併せて変化したデータ構造に合わせてプロパティ名やチェーンを調整した感じです。</p> <h3 id="blog-post.js"><a href="#blog-post.js">blog-post.js</a></h3> <pre><code class="javascript">import React from "react" import { Link, graphql } from "gatsby" import Bio from "../components/bio" import Layout from "../components/layout" import SEO from "../components/seo" import { rhythm, scale } from "../utils/typography" const BlogPostTemplate = ({ data, pageContext, location }) => { const post = data.markdownRemark // const siteTitle = data.site.siteMetadata.title const { previous, next } = pageContext return ( <Layout location={location} title="Home"> <SEO title={post.frontmatter.title} description={post.frontmatter.description || post.excerpt} /> <article> <header> <h1 style=<span>{</span><span>{</span> marginBottom: 0, <span>}</span><span>}</span> > {post.frontmatter.title} </h1> <p style=<span>{</span><span>{</span> ...scale(-1 / 5), display: `block`, marginBottom: rhythm(1), <span>}</span><span>}</span> > {post.frontmatter.date} </p> </header> <section dangerouslySetInnerHTML=<span>{</span><span>{</span> __html: post.html <span>}</span><span>}</span> /> <hr style=<span>{</span><span>{</span> marginBottom: rhythm(1), <span>}</span><span>}</span> /> <footer> <Bio /> </footer> </article> <nav> <ul style=<span>{</span><span>{</span> display: `flex`, flexWrap: `wrap`, justifyContent: `space-between`, listStyle: `none`, padding: 0, <span>}</span><span>}</span> > <li> {previous && ( <Link to={previous.fields.slug} rel="prev"> ← {previous.frontmatter.title} </Link> )} </li> <li> {next && ( <Link to={next.fields.slug} rel="next"> {next.frontmatter.title} → </Link> )} </li> </ul> </nav> </Layout> ) } export default BlogPostTemplate export const pageQuery = graphql` query BlogPostBySlug($slug: String!) { site { siteMetadata { title } } markdownRemark(fields: { slug: { eq: $slug } }) { id excerpt(pruneLength: 160) html frontmatter { title date(formatString: "MMMM DD, YYYY") description } } } ` </code></pre> <p>これを以下のように改修。</p> <pre><code class="javascript">import React from "react" import { Link, graphql } from "gatsby" import Bio from "../components/bio" import Layout from "../components/layout" import SEO from "../components/seo" import { rhythm, scale } from "../utils/typography" import marked from "marked" const BlogPostTemplate = ({ data, pageContext, location }) => { const post = data.microcmsHogeHogeBlog // const siteTitle = data.site.siteMetadata.title const { previous, next } = pageContext return ( <Layout location={location} title="Home"> <SEO title={post.title} description={post.keywords} /> <article> <header> <h1 style=<span>{</span><span>{</span> marginBottom: 0, <span>}</span><span>}</span> > {post.title} </h1> <p style=<span>{</span><span>{</span> ...scale(-1 / 5), display: `block`, marginBottom: rhythm(1), <span>}</span><span>}</span> > {post.date} </p> </header> <section dangerouslySetInnerHTML=<span>{</span><span>{</span> __html: marked(post.body) <span>}</span><span>}</span> /> <hr style=<span>{</span><span>{</span> marginBottom: rhythm(1), <span>}</span><span>}</span> /> <footer> <Bio /> </footer> </article> <nav> <ul style=<span>{</span><span>{</span> display: `flex`, flexWrap: `wrap`, justifyContent: `space-between`, listStyle: `none`, padding: 0, <span>}</span><span>}</span> > <li> {previous && ( <Link to={'../'+previous.slug} rel="prev"> ← {previous.title} </Link> )} </li> <li> {next && ( <Link to={'../'+next.slug} rel="next"> {next.title} → </Link> )} </li> </ul> </nav> </Layout> ) } export default BlogPostTemplate export const pageQuery = graphql` query BlogPostBySlug($slug: String!) { site { siteMetadata { title } } microcmsHogeHogeBlog(slug: { eq: $slug }) { id keywords title updatedAt slug revisedAt publishedAt date(formatString: "YYYY/MM/DD") createdAt body } } ` </code></pre> <p>こちらも <code>blog-list.tsx</code> と同様 GraphQL の変更とそれに伴うプロパティ名やチェーンの調整がメインです。本文の Markdown は冒頭の通り時短のため <code>marked</code> を使いました。</p> <p>こうして見てみると、 GraphQL に慣れるまでかなり紆余曲折した気がするのですが、 <code>blog-list.tsx</code> や <code>blog-post.js</code> は特に出来上がったコードがあまり元から変化していませんね……。</p> <h2 id="microCMS で APIキー を参照"><a href="#microCMS+%E3%81%A7+API%E3%82%AD%E3%83%BC+%E3%82%92%E5%8F%82%E7%85%A7">microCMS で APIキー を参照</a></h2> <p>microCMS で APIキー を控えます。</p> <p><a href="https://crieit.now.sh/upload_images/949bd68fc9e6b312266c6dd7d831ade5612a583ed1bd6.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/949bd68fc9e6b312266c6dd7d831ade5612a583ed1bd6.jpg?mw=700" alt="APIキーの取得" /></a></p> <p>「サービス設定」→「APIキー」と進みます。今回必要なのは「X-API-KEY」。これを控えておきます。</p> <h2 id="Vercel の設定"><a href="#Vercel+%E3%81%AE%E8%A8%AD%E5%AE%9A">Vercel の設定</a></h2> <p>続いて Vercel の設定へ。</p> <h3 id="環境変数の設定"><a href="#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%AE%E8%A8%AD%E5%AE%9A">環境変数の設定</a></h3> <p>まずは環境変数の設定。</p> <p><a href="https://crieit.now.sh/upload_images/57a8cc2a08598bf78b631bbd9da2a024612a5842798b2.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/57a8cc2a08598bf78b631bbd9da2a024612a5842798b2.jpg?mw=700" alt="環境変数の設定" /></a></p> <p>プロジェクトを選択後、「Settings」から「Enviroment Variables」へ。</p> <p>「NAME」にコードに記載したキーの名前を付けて、実際の値を入力します。画面では APIキー なので上述の microCMS の管理画面で控えた APIキー を入力。</p> <p><a href="https://crieit.now.sh/upload_images/3bbfd322b71fb32df23894afdd5853d5612a584583337.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/3bbfd322b71fb32df23894afdd5853d5612a584583337.jpg?mw=700" alt="環境変数の設定2" /></a></p> <p>他、必要な変数をセットします。</p> <h3 id="Deploy Hooks を登録"><a href="#Deploy+Hooks+%E3%82%92%E7%99%BB%E9%8C%B2">Deploy Hooks を登録</a></h3> <p>次に Deploy Hooks をひっかけます。</p> <p>プロジェクトの「Settings」から今度は「Git」へ。</p> <p><a href="https://crieit.now.sh/upload_images/4f89b78a73ad66c2fab07245a0b599ac612a58482a821.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4f89b78a73ad66c2fab07245a0b599ac612a58482a821.jpg?mw=700" alt="Deploy hooks" /></a></p> <p>Deploy Hooks に名前を入力、ブランチは通常は <code>main</code> になるかと。</p> <p><a href="https://crieit.now.sh/upload_images/792d34407a2e1061c785101139250868612a584aeed86.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/792d34407a2e1061c785101139250868612a584aeed86.jpg?mw=700" alt="Deploy Hooks を控える" /></a></p> <p>登録されたら Hook の URL を控えます。</p> <h2 id="microCMS の設定"><a href="#microCMS+%E3%81%AE%E8%A8%AD%E5%AE%9A">microCMS の設定</a></h2> <p>お次は microCMS の画面へ。</p> <p><a href="https://crieit.now.sh/upload_images/16478a5b69bd9cb593abea925cd2c925612a584d45b74.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/16478a5b69bd9cb593abea925cd2c925612a584d45b74.jpg?mw=700" alt="Webhook" /></a></p> <p>サービスの管理画面から「API設定」→「Webhook」へ。</p> <p><a href="https://crieit.now.sh/upload_images/192e10d26803bfa0dcf31470a2553f63612a58505802c.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/192e10d26803bfa0dcf31470a2553f63612a58505802c.jpg?mw=700" alt="カスタム通知" /></a></p> <p>Vercel は一覧にはないので「カスタム通知」を選択。</p> <p><a href="https://crieit.now.sh/upload_images/b93547fbc6ca4f11e11986ec1869d7d5612a5853d80e0.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b93547fbc6ca4f11e11986ec1869d7d5612a5853d80e0.jpg?mw=700" alt="Vercel の Hook を設定" /></a></p> <p>名前は任意の名前を。</p> <p>Hook の URL に先ほど控えた URL を入力します。</p> <p><a href="https://crieit.now.sh/upload_images/39693fd99f7447504a37a4bfd38afea0612a58568376c.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/39693fd99f7447504a37a4bfd38afea0612a58568376c.jpg?mw=700" alt="Hook の権限" /></a></p> <p>権限はこのような感じ。基本的に公開されているデータに変更が加わったら Hook が通知を行うものとします。</p> <h2 id="テスト投稿"><a href="#%E3%83%86%E3%82%B9%E3%83%88%E6%8A%95%E7%A8%BF">テスト投稿</a></h2> <p><a href="https://crieit.now.sh/upload_images/58704ee832c977a18796e1a3e05c2c8f612a5859db3dd.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/58704ee832c977a18796e1a3e05c2c8f612a5859db3dd.jpg?mw=700" alt="記事を試しに追加" /></a></p> <p>ここまで設定できたら、試しに記事を新しく追加してみます。</p> <p><a href="https://crieit.now.sh/upload_images/b79d4f2666aaa6c98f4c010011c12e6f612a585d5f4c4.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b79d4f2666aaa6c98f4c010011c12e6f612a585d5f4c4.jpg?mw=700" alt="Vercel 側が反応" /></a></p> <p>すると、 Vercel 側でプロジェクトが反応してビルドが走り始めました。</p> <p><a href="https://crieit.now.sh/upload_images/75c7890bf380c4b476fea57535096623612a586141166.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/75c7890bf380c4b476fea57535096623612a586141166.jpg?mw=700" alt="デプロイ完了" /></a></p> <p>microCMS に記事を追加すると、 Github のリポジトリに push することなく勝手にビルドが走ってサイトが更新されることが確認できました。</p> <p>これで Gatsby.js + Vercel + microCMS で JAMStack なブログの仕組みが構築できました。実験成功です。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="改修"><a href="#%E6%94%B9%E4%BF%AE">改修</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/k1_style/scraps/07cce0dd3611e3">Gatsby + microcms + Vercel でブログを作って公開したい</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/plugins/gatsby-source-microcms/">gatsby-source-microcms | Gatsby</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/rokki188/articles/948d53199508c7">microCMS + Next.js でJamStackブログを作ってみた</a></li> </ul> <h3 id="コード、GraphQL、microCMSの設定、環境変数"><a href="#%E3%82%B3%E3%83%BC%E3%83%89%E3%80%81GraphQL%E3%80%81microCMS%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%80%81%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0">コード、GraphQL、microCMSの設定、環境変数</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://blog.microcms.io/microcms-next-jamstack-blog/">microCMS + Next.jsでJamstackブログを作ってみよう | microCMSブログ</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://blog.microcms.io/gatsby-microcms-media/">GatsbyJS + microCMSでJamstackなオウンドメディアを作ろう | microCMSブログ</a></li> </ul> <h3 id="Vercel の環境変数"><a href="#Vercel+%E3%81%AE%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0">Vercel の環境変数</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://vercel.com/docs/environment-variables">Vercel – Environment Variables - Vercel Documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.snorerelax.com/posts/tech-vercel-environment/">Vercelで環境変数を設定する | Next.js Blog Example with すのりら</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/Slowhand0309/items/f954237520d343fa9e4c">Vercelで設定した環境変数をNext.jsで使用する - Qiita</a></li> </ul> <h4 id="環境変数の分かち書き"><a href="#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%81%AE%E5%88%86%E3%81%8B%E3%81%A1%E6%9B%B8%E3%81%8D">環境変数の分かち書き</a></h4> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://blog.gaji.jp/2020/06/09/4030/">GatsbyJSで開発環境にのみページを存在させる方法 ++ Gaji-Laboブログ</a></li> </ul> <h3 id="gatsby-plugin-feed"><a href="#gatsby-plugin-feed">gatsby-plugin-feed</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/plugins/gatsby-plugin-feed/">gatsby-plugin-feed | Gatsby</a></li> </ul> <p>基本 Gatsby.js のプラグインのページはシンプルなサンプルなので <code>http://localhost:8000/___graphql</code> で GraphQL を試して感覚をつかむ方が早かった気がします。</p> arm-band tag:crieit.net,2005:PublicArticle/17615 2021-08-25T22:14:35+09:00 2021-08-29T20:33:12+09:00 https://crieit.net/posts/publish-blog-by-gatsvyjs-vercel-20210825 Gatsby.js + Vercel でブログを作ってみる <p>兼ねてより試してみたかった Gatsby.js + Vercel の組み合わせに着手してみました。</p> <h2 id="1. Gatsby.js のインストールと Gitリポジトリ の準備"><a href="#1.+Gatsby.js+%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%A8+Git%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA+%E3%81%AE%E6%BA%96%E5%82%99">1. Gatsby.js のインストールと Gitリポジトリ の準備</a></h2> <p>まずは Gatsby.js のプロジェクトを作って Gitホスティングサービス に push します。</p> <pre><code class="bash">> yarn init ## 略 success Saved package.json Done in 2.33s. </code></pre> <p>適当なディレクトリで <code>package.json</code> を生成。</p> <pre><code class="bash">> yarn add gatsby-cli ## 略 Done in 26.05s. </code></pre> <p>続いて <code>gatsby-cli</code> をインストールします。一応グローバルではなくローカルで。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/static/203f352fa59959029a106ffe821ad0bb/85e35/93d59e2b139dc14ee2118ff7410333c9.webp">: Gatsby Starter | Gatsby</a></li> </ul> <p>今回のテーマはこれにします。</p> <pre><code class="bash">> gatsby new mptotkb https://github.com/renyuanz/leonids ╔════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ Gatsby collects anonymous usage analytics ║ ║ to help improve Gatsby for all users. ║ ║ ║ ║ If you'd like to opt-out, you can use `gatsby telemetry --disable` ║ ║ To learn more, checkout https://gatsby.dev/telemetry ║ ║ ║ ╚════════════════════════════════════════════════════════════════════════╝ info Creating new site from git: https://github.com/renyuanz/leonids.git Cloning into 'mptotkb'... remote: Enumerating objects: 60, done. remote: Counting objects: 100% (60/60), done. remote: Compressing objects: 100% (44/44), done. Receiving objects: 100% (60/60), 542.69 KiB | 5.32 MiB/s, done.eceiving objects: 100% (60/60) success Created starter directory layout info Installing packages... info Preferred package manager set to "npm" ## 略 Your new Gatsby site has been successfully bootstrapped. Start developing it by running: cd mptotkb gatsby develop </code></pre> <p>プロジェクト作成完了。</p> <pre><code class="bash">> cd mptotkb > gatsby develop ## 略 info Hi from the Gatsby maintainers! Based on what we see in your site, these coming features may help you. All of these can be enabled within gatsby-config.js via flags (samples below) Preserve webpack's Cache (https://github.com/gatsbyjs/gatsby/discussions/28331), which changes Gatsby's cache clearing behavior to not clear webpack's cache unless you run "gatsby clean" or delete the .cache folder manually. Here's how to try it: module.exports = { flags: { PRESERVE_WEBPACK_CACHE: true }, plugins: [...] } ⠀ You can now view gatsby-starter-leonids in the browser. ⠀ http://localhost:8000/ ⠀ View GraphiQL, an in-browser IDE, to explore your site's data and schema ⠀ http://localhost:8000/___graphql ⠀ Note that the development build is not optimized. To create a production build, use gatsby build ⠀ success Building development bundle - 48.261s </code></pre> <p>OK。完了しました。</p> <p>最初は手始めということでこの状態でいったん push しておきたいと思います。 Gitリポジトリ に登録するのは親ディレクトリではなく作成したプロジェクト (今回は <code>mptotkb</code> 下) です。</p> <p><a href="https://crieit.now.sh/upload_images/c767731cd51656a0324d46f4f80e7af261263f776ecdc.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c767731cd51656a0324d46f4f80e7af261263f776ecdc.jpg?mw=700" alt="GitHub でリポジトリを作成" /></a></p> <p>GitHub でリポジトリを作成して、このリポジトリに先ほどのプロジェクトを push 。ちなみにリポジトリは public である必要があります。</p> <h2 id="2. Vercel と Gitホスティングサービス の連携"><a href="#2.+Vercel+%E3%81%A8+Git%E3%83%9B%E3%82%B9%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9+%E3%81%AE%E9%80%A3%E6%90%BA">2. Vercel と Gitホスティングサービス の連携</a></h2> <p>次に Vercel でアカウントを作ります。</p> <p><a href="https://crieit.now.sh/upload_images/77ce0ff86a6de3b873bb06d7f1de0c8e61263f822ba39.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/77ce0ff86a6de3b873bb06d7f1de0c8e61263f822ba39.jpg?mw=700" alt="Vercel で Sign Up をクリック" /></a></p> <p>Sign Up をクリック。</p> <p><a href="https://crieit.now.sh/upload_images/43666c5f16041fc59fdb8d593a9b955561263f8d448dd.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/43666c5f16041fc59fdb8d593a9b955561263f8d448dd.jpg?mw=700" alt="GitHub と連携" /></a></p> <p>今回は GitHub を使用しているので「Continue with GitHub」を選択。</p> <p><a href="https://crieit.now.sh/upload_images/1f94d557606bdff1448bcb340a9d56b861263f98c6f6f.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/1f94d557606bdff1448bcb340a9d56b861263f98c6f6f.jpg?mw=700" alt="Vercel のアクセスを認証して権限を付与" /></a></p> <p>「Authorize Vercel」で Vercel のアクセスを認証します。</p> <p><a href="https://crieit.now.sh/upload_images/0e40bf8259a51bb7dea9189426fb3f5961263faa0614f.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0e40bf8259a51bb7dea9189426fb3f5961263faa0614f.jpg?mw=700" alt="Vercel にログイン成功" /></a></p> <p>ログインできました。</p> <h2 id="3. リポジトリのインポート"><a href="#3.+%E3%83%AA%E3%83%9D%E3%82%B8%E3%83%88%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88">3. リポジトリのインポート</a></h2> <p><a href="https://crieit.now.sh/upload_images/9b1b79698c430d0d958f3d770e4c62bf61263fbda9d17.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/9b1b79698c430d0d958f3d770e4c62bf61263fbda9d17.jpg?mw=700" alt="ダッシュボードから「New Project」" /></a></p> <p>一度中断してしまったのでダッシュボードから。ここからであれば「New Project」で次へ進みます。</p> <p><a href="https://crieit.now.sh/upload_images/490785baeef1275ccb89cdb6d4a5db7861263fc869e4e.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/490785baeef1275ccb89cdb6d4a5db7861263fc869e4e.jpg?mw=700" alt="Import Git Repository から「Add GitHub Org or Account」" /></a></p> <p>Import Git Repository から「Add GitHub Org or Account」。</p> <p><a href="https://crieit.now.sh/upload_images/92eb299d6668bdba87d40c089976f9fc61263fd566948.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/92eb299d6668bdba87d40c089976f9fc61263fd566948.jpg?mw=700" alt="インポートするリポジトリを選択" /></a></p> <p>インポートするリポジトリを選択します。今回は1つのリポジトリだけに Vercel に連携させます。</p> <p><a href="https://crieit.now.sh/upload_images/95606ef51dd06912c5fe460301a1d09361263fe080aa9.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/95606ef51dd06912c5fe460301a1d09361263fe080aa9.jpg?mw=700" alt="リポジトリの確認" /></a></p> <p>リポジトリの確認です。使用しているフレームワークが自動判別されるのはありがたいですね。</p> <p><a href="https://crieit.now.sh/upload_images/676cf8e4e4edee1f6c692cd0a445524e61263fea8e98f.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/676cf8e4e4edee1f6c692cd0a445524e61263fea8e98f.jpg?mw=700" alt="チームの作成は「Skip」" /></a></p> <p>次の画面でフローに沿って設定を進めていきます。最初に Vercel のチームを作成する画面が有効になっていますが、今回は個人なので「Skip」。</p> <p><a href="https://crieit.now.sh/upload_images/fa75aa33dba45c6b8029ab0a0a3695bc61263ff3507ed.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/fa75aa33dba45c6b8029ab0a0a3695bc61263ff3507ed.jpg?mw=700" alt="Configure Project" /></a></p> <p>プロジェクトの設定。出力の設定や環境変数を設定できますが、今回はそのままで。</p> <p><a href="https://crieit.now.sh/upload_images/64a16b6c3867805df977e0ac9704d57361263ffb9d76a.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/64a16b6c3867805df977e0ac9704d57361263ffb9d76a.jpg?mw=700" alt="デプロイ中" /></a></p> <p>デプロイ中。</p> <p><a href="https://crieit.now.sh/upload_images/eb14aeca4ce6114b32dd4f90697d064361264004d26ec.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/eb14aeca4ce6114b32dd4f90697d064361264004d26ec.jpg?mw=700" alt="デプロイされた画面を実際に確認" /></a></p> <p>少し待つとデプロイが完了します。そこでURLから実際の画面を表示させると……きちんとデプロイされていました!</p> <p>ここまでで Gatsby.js + Vercel でブログを作成することができました。</p> <p>今後の課題としては以下の通り。</p> <ul> <li>サイト名や著者情報といった基本情報のカスタマイズ</li> <li>どうやらブログ記事の Markdown ファイルを生成する CLI やユーティリティはなさそうで、手作業で記事をちまちま作っていくのは手間なので記事リソースを別の場所 (<a href="https://crieit.net/posts/gatsby-vercel-microcms-cooprate-20210829-1">Headless CMS</a> とか) で管理して <a href="https://crieit.net/posts/gatsby-vercel-microcms-cooprate-20210829-2">JAMStack にする</a></li> </ul> <p>あとは折角なので GraphQL にも少しは触ってみたいところですね。</p> <h2 id="参考"><a href="#%E5%8F%82%E8%80%83">参考</a></h2> <h3 id="Gatsby.js + Vercel"><a href="#Gatsby.js+%2B+Vercel">Gatsby.js + Vercel</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://webcraftlog.net/gatsbyjs-site-deploy-to-vercel/">【ZEIT Now】Gatsbyサイトを無料サーバーVercelで公開する方法を徹底解説 | WebCraftLog</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://zenn.dev/k1_style/scraps/07cce0dd3611e3">Gatsby + microcms + Vercel でブログを作って公開したい</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://vercel.com/docs/git">Vercel – Git - Vercel Documentation</a> <ul> <li>参考記事とは画面構成が変わっていたため、最終的には Vercel のドキュメントが頼りでした</li> </ul></li> </ul> <h3 id="Gatsby.js のテーマ"><a href="#Gatsby.js+%E3%81%AE%E3%83%86%E3%83%BC%E3%83%9E">Gatsby.js のテーマ</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/starters/?c=Blog">Gatsby Starters: Library | Gatsby</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://www.gatsbyjs.com/starters/renyuanz/leonids">: Gatsby Starter | Gatsby</a></li> </ul> <h3 id="Vercel"><a href="#Vercel">Vercel</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://dev.classmethod.jp/articles/vercel/">NetlifyキラーのVercelでウェブサイトをホストしたら簡単すぎて笑顔になった | DevelopersIO</a></li> </ul> <h3 id="Gatsby.js"><a href="#Gatsby.js">Gatsby.js</a></h3> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/k-penguin-sato/items/7554e5e7e90aa10ae225">GatsbyとNetlifyで簡単にブログを作成 - Qiita</a></li> </ul> arm-band tag:crieit.net,2005:PublicArticle/16704 2021-03-01T01:31:55+09:00 2021-03-01T01:31:55+09:00 https://crieit.net/posts/Next-js-Vercel-Recoil-Material-Table-AWS Next.jsとVercelとRecoilとMaterial Tableを使ってAWSのステータスダッシュボードを作ってみた話 <h2 id="AWSのステータス確認難しいよね"><a href="#AWS%E3%81%AE%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E7%A2%BA%E8%AA%8D%E9%9B%A3%E3%81%97%E3%81%84%E3%82%88%E3%81%AD">AWSのステータス確認難しいよね</a></h2> <p>AWSを使ったことのある人ならばわかると思いますが、公式がAWSの障害情報を掲載する<a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/">AWS Service Health Dashboard</a>があまり使いやすくないです。</p> <p><img src="https://i.imgur.com/XghDulZ.png" alt="img" /></p> <p>それぞれのリージョンの障害がRSSで配信される形式になっているのですが、わざわざRSSを登録するのもめんどくさいし、Slackとかの連携に乗っけるのもそれはそれで便利なのですが、そもそもSlackを見ていないほかの人でも障害情報を共有したいです。</p> <p>実は、AWS Service Health Dashboardの情報はJSONで取得することができます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/data.json">https://status.aws.amazon.com/data.json</a></p> <p>こちらのJSONを活用して勉強がてら使いやすいダッシュボードを作っていきます。</p> <h2 id="クビになるぞ!"><a href="#%E3%82%AF%E3%83%93%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%9E%EF%BC%81">クビになるぞ!</a></h2> <p>最近、これといった新しい技術に触れておらず、このままだとクビになりそうなので、そろそろ重い腰を上げてNext.jsを勉強することにしました。</p> <p>また、Next.jsを使う場合はVercelが便利だよーとのことですので、こちらも使っていきます。</p> <h2 id="Next.js"><a href="#Next.js">Next.js</a></h2> <p>Next.jsではpages/api配下に格納したコードについては、サーバーサイドとして振る舞います。</p> <p>クライアントから直接status情報がかかれたJSONを読みとってもよかったのですが、HTMLの面倒なサニタイジング処理やら、値の補完など面倒なことはサーバーサイドに持ってこようということで、<br /> statusJSONを取得して、フロントに返却するサーバーコードを書いていきます。</p> <p>次のようなコードになりました。</p> <pre><code class="typescript">import { NextApiRequest, NextApiResponse } from 'next' import axios from 'axios' export interface AwsStatusResp { archive: AwsStatusArchive[] } export interface AwsStatusArchive { service_name: string summary: string date: string status: string details: string description: string service: string } const handler = (req: NextApiRequest, res: NextApiResponse) => { axios .get<AwsStatusResp>('https://status.aws.amazon.com/data.json') .then((resp) => { const handlerResp = resp.data.archive.map((x) => ({ // eslint-disable-next-line @typescript-eslint/camelcase service_name: x.service_name, summary: x.summary, region: x.service.includes('management-console') ? 'global' : x.service.split('-').slice(1).join('-') === '' ? 'global' : x.service.split('-').slice(1).join('-'), date: x.date, status: x.status, details: x.details, service: x.service.includes('management-console') ? 'management-console' : x.service.split('-')[0], description: x.description .replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '') .replace(/&nbsp;/g, '\n'), })) res.statusCode = 200 // eslint-disable-next-line no-console console.log(handlerResp) res.json(handlerResp) }) .catch((error) => { console.error(error.response) res.statusCode = error.response.status || 500 res.statusMessage = error.response.statusText || 'InternalServerError' res.json({ error: error.response.statusText || 'InternalServerError' }) }) } export default handler </code></pre> <p>注意点として、必ずハンドラーはexport defaultを指定してあげないこと以外はいたって直感的なコードとなっております。</p> <p>Vercelに載っけるとわかるのですが、こちらのコード、Lambdaにデプロイされることになります。たしかに見覚えある感じですね。</p> <p>また、Next.jsと関係ないのですが、axiosのレスポンスに型がつけられるって知ってましたか?</p> <pre><code class="typescript">export interface AwsStatusResp { archive: AwsStatusArchive[] } export interface AwsStatusArchive { service_name: string summary: string date: string status: string details: string description: string service: string } axios .get<AwsStatusResp>('https://status.aws.amazon.com/data.json') .then((resp) => { .......... </code></pre> <h2 id="Material Table"><a href="#Material+Table">Material Table</a></h2> <p>Material UI準拠のテーブルとして、Material Tableなるものがありましたので今回採用することにしました。</p> <pre><code class="typescript">import MaterialTable from 'material-table' import tableIcons from '../components/tableIcons' <MaterialTable icons={tableIcons} columns={[ { title: 'Service Name', field: 'service_name' }, { title: 'Service', field: 'service', width: 10 }, { title: 'Region', field: 'region', lookup: regionNameMapping }, { title: 'Summary', field: 'summary' }, { title: 'Date (' + dayjs.tz.guess() + ')', field: 'date', render: (rowData) => ( <div> {dayjs .unix(Number(rowData.date)) .format('YYYY-MM-DDTHH:mm:ssZ[Z]')} </div> ), defaultSort: 'desc', type: 'string', }, { title: 'Status', field: 'status', lookup: statusMapping, }, ]} data={aws} detailPanel={[ { tooltip: 'Details', render: (rowData) => { return ( <> <div className="title">{rowData.summary}</div> <div className="description"> {dayjs .unix(Number(rowData.date)) .format('YYYY-MM-DDTHH:mm:ss')}{' '} {rowData.service_name} </div> <div className="code">{rowData.description}</div> </> ) }, }, ]} options=<span>{</span><span>{</span> filtering: true, grouping: true, exportButton: true, exportFileName: 'exported', headerStyle: { backgroundColor: '#e77f2f', color: '#FFF', }, <span>}</span><span>}</span> isLoading={loading} actions={[ { // Issue: https://github.com/mbrn/material-table/issues/51 //@ts-ignore icon: tableIcons.BarChartIcon, tooltip: 'Show Bar Chart', isFreeAction: true, disabled: loading, onClick: async () => { setShowGraph(!showG) }, }, { // Issue: https://github.com/mbrn/material-table/issues/51 //@ts-ignore icon: tableIcons.Refresh, tooltip: 'Refresh Data', isFreeAction: true, disabled: loading, onClick: async () => { setLoading(true) await getAwsStatus() }, }, ]} title={ <div className="header"> <img src="/awslogo.png" /> <a href="https://aws-health-dashboard.vercel.app/"> AWS Health Dashboard </a> </div> } /> </code></pre> <p>使い方もシンプルかつ比較的高機能でいい感じです。</p> <p>いい感じですが後述するRecoilとの相性問題とDatetimeの扱いが微妙なのがツラミでした。</p> <p>本当はDate型を渡してあげるとSearchableの際、カレンダーでの絞り込みができるのかなと思ったのですが、こちらがうまくいきませんでした。</p> <p>あと、微妙に型もおかしく例えば、actionsはactionを複数指定することができるはずですが、型チェックで怒られるので、仕方なくts-ignoreしてます。</p> <p>あなたが直せばいいじゃんアゼルバイジャンって言われそうですが、めんどくさくなってしまいIssueだけあげてしまいました。申し訳ねぇ...。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mbrn/material-table/issues/2762">https://github.com/mbrn/material-table/issues/2762</a></p> <h2 id="Recoil"><a href="#Recoil">Recoil</a></h2> <p>RecoilとはReactの新しい状態管理ライブラリで、いわゆるReact HooksでGlobal Storeを作ろうというものです。</p> <p>基本的な使い方はまず、storeとしてatomという共有ステートを作成します。</p> <p>atomのkeyはプロジェクトで一意にする必要がありますが、今回はそこまで大規模なプロジェクトではないのでawsとかいうクソ名をつけてます。</p> <p>storeなので、store/aws.ts として格納します。</p> <pre><code class="typescript">import { atom } from 'recoil' const awsState = atom({ key: 'aws', default: [ { // eslint-disable-next-line @typescript-eslint/camelcase service_name: 'Auto Scaling (N. Virginia)', summary: '[RESOLVED] Example Error', date: '1542849575', status: '1', details: '', description: 'The issue has been resolved and the service is operating normally.', service: 'autoscaling', region: 'us-east-1', }, ], dangerouslyAllowMutability: true, }) export default awsState </code></pre> <p>次にステートを共有したいコンポーネントのルートにRecoilRootを設置します。</p> <p>Next.jsの場合、_app.tsxが全ページのルートにあたるのでここに置けばいいですね。</p> <pre><code class="typescript">import { AppProps } from 'next/app' import Head from 'next/head' import { RecoilRoot } from 'recoil' import React from 'react' const App = ({ Component, pageProps }: AppProps) => ( <> <RecoilRoot> <Head> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <title>AWS Health Dashboard</title> </Head> <Component {...pageProps} /> </RecoilRoot> </> ) export default App </code></pre> <p>そして、利用するときはuseRecoilStateをReact Hooksのように利用するだけです。簡単ですね。</p> <pre><code class="typescript">import React, { useState, useEffect } from 'react' import { useRecoilState } from 'recoil' import awsState from '../store/aws' import showGraph from '../store/showGraph' import axios from 'axios' import dayjs from 'dayjs' dayjs.extend(utc) dayjs.extend(timezone) const Alert = (props: AlertProps) => { return <MuiAlert elevation={6} variant="filled" {...props} /> } export const Table = (): JSX.Element => { // 20200112: dangerouslyAllowMutabilityでできた const [aws, setAws] = useRecoilState(awsState) const [showG, setShowGraph] = useRecoilState(showGraph) const [loading, setLoading] = useState(true) const [slackBarOpen, setSlackBarOpen] = React.useState(false) const [apiErrorMsg, setApiErrorMsg] = React.useState('') useEffect(() => { getAwsStatus() setLoading(false) }, []) const getAwsStatus = () => { axios .get('/api/aws') .then((resp) => { setAws(resp.data) setLoading(false) }) .catch((error) => { console.error(error.response) setSlackBarOpen(true) setApiErrorMsg(error.response.statusText || 'Error') setAws([]) setLoading(false) }) } </code></pre> <p>stateの読み込みはgetterから、書き込みはsetterから行います。</p> <p>React Hooksに慣れていれば簡単ですね。</p> <h2 id="思わぬ落とし穴 Material TablesでRecoilが使えない"><a href="#%E6%80%9D%E3%82%8F%E3%81%AC%E8%90%BD%E3%81%A8%E3%81%97%E7%A9%B4+Material+Tables%E3%81%A7Recoil%E3%81%8C%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84">思わぬ落とし穴 Material TablesでRecoilが使えない</a></h2> <p>Recoilのatomは基本値の書き換えはset stateを使うことが求められます。ですが、material tablesはテーブルを作るときにdataにIDの書き込みが発生するようでそのままだと怒られてしまいます。</p> <pre><code>Cannot add property tableData, object is not extensible </code></pre> <p>これの解決策はRecoilにstateへの直接的な書き換えを許可することです。こちらはatomのoptionでdangerouslyAllowMutabilityを有効にすることで解決できます。</p> <pre><code class="typescript">import { atom } from 'recoil' const awsState = atom({ key: 'aws', default: [ { }, ], dangerouslyAllowMutability: true, }) </code></pre> <p>これがわかるのに半日くらい使っちまいました。</p> <h2 id="Chart.js"><a href="#Chart.js">Chart.js</a></h2> <p>さて、無事にRecoilでstateの共有ができたのでせっかくなので別コンポーネントも作ります。</p> <p>意味があるかどうか不明ですが、AWSの障害発生状況を可視化してみようと思います。</p> <p>ということで、採用したのがChart.js。</p> <p>次のようにデータを渡すだけできれいめなグラフを書いてくれます。</p> <pre><code class="typescript">import { useRecoilValue } from 'recoil' import awsState from '../store/aws' import React from 'react' import { regionNameMapping, } from './const' import BarGraph from './barGraph' export const AlertPerRegion = (): JSX.Element => { const aws = useRecoilValue(awsState) const labels = Array.from( new Set(aws.map((data) => regionNameMapping[data.region])) ) const data = [] for (const r of labels) { data.push( aws .map((data) => regionNameMapping[data.region]) .reduce((total, x) => { return x === r ? total + 1 : total }, 0) ) } return ( <div className="container"> <BarGraph labels={labels} data={data} title="Alert per region" /> </div> ) } </code></pre> <p>どうでもいい実装ですが、各グラフを一覧で見れる画面を用意し、実際のグラフは遷移先で表示するようにしてます。</p> <p><img src="https://i.imgur.com/tfnpq4w.png" alt="img" /></p> <p><img src="https://i.imgur.com/hpJ70fR.png" alt="img" /></p> <h2 id="Vercelにデプロイ"><a href="#Vercel%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">Vercelにデプロイ</a></h2> <p>さて、実装ができたので後はVercelにあげるだけです。</p> <p>もうここはほとんど書くことがないのですが、Next.jsで作ったアプリケーションはVercelでレポジトリと使っているフレームワークを設定するだけで簡単にデプロイ出来てしまいます。</p> <p>これはすごい。</p> <h2 id="完成"><a href="#%E5%AE%8C%E6%88%90">完成</a></h2> <p>ということで、AWS Health Dashboardが完成しました。</p> <p>アクセスすると、Next.jsのapiをコールし、AWSのstatusを取得加工したものを返却します。</p> <p>フロントでは受け取ったデータをRecoilのatomに格納しつつ、material tableで表として描画します。</p> <p>また右上のグラフボタンを押すことで色々な切り口の可視化を行うことができます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aws-health-dashboard.vercel.app/">https://aws-health-dashboard.vercel.app/</a></p> <p>できれば使う場面にならないことを祈りつつ、ご活用いただければとおもいます。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>食わず嫌いでやらなかったNext.js+Recoilをやってみましたが、楽しく実装ができました。</p> <h2 id="2021/02/20追記"><a href="#2021%2F02%2F20%E8%BF%BD%E8%A8%98">2021/02/20追記</a></h2> <p>2021/02/19~20にかけて起きた<a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/rss/ec2-ap-northeast-1.rss">AWS EC2障害</a>ですが、本ダッシュボードでは更新がされませんでした。</p> <p>どうやら、data.jsonはRSSとは違い、同期的に更新されないようです。</p> <p>大変ご迷惑をおかけしました。改めて、改修しRSS更新にも対応できるように頑張ります。</p> tubone24 tag:crieit.net,2005:PublicArticle/16121 2020-10-10T23:46:11+09:00 2020-10-11T07:07:33+09:00 https://crieit.net/posts/Vercel-API-Routes-OGP-2020 VercelにてNext.jsのAPI RoutesでOGPを作成する 2020年版 <p>VercelにてNext.jsのAPI Routesでファイルが読み込めるようになっていました! 元々、API Routesでは別のファイルの読み込みができませんでした。そのため下記の記事のように別途node builderを利用していましたが、もうそれも必要なくなり、開発もデプロイもシンプルなAPI Routesとして実行できるようになっているようで、非常に簡単になりました。</p> <p><a href="https://crieit.net/posts/Vercel-Zeit-Now-Next-js-node-canvas-OGP">Vercel(元ZeitのNow)にてNext.jsでnode-canvasを使ってOGP</a></p> <h2 id="どういうことか"><a href="#%E3%81%A9%E3%81%86%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%81%8B">どういうことか</a></h2> <p>このissueで話が進んでおり、このコメントが解決方法になっています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/vercel/next.js/issues/8251#issuecomment-657770901">Next.js API routes (and pages) should support reading files · Issue #8251 · vercel/next.js</a></p> <p>つまり、</p> <blockquote> <p>path.resolve('./public/ts-data.csv')</p> </blockquote> <p>というパス指定でファイルが読み込めるようになっています! めでたい。</p> <h2 id="具体例"><a href="#%E5%85%B7%E4%BD%93%E4%BE%8B">具体例</a></h2> <p>具体的にはこういうコードでVercelでも正常に動作しました。フォントを読み込んでのテキスト描画も、画像の描画も、正常にできています。</p> <pre><code class="typescript">import { NextApiRequest, NextApiResponse } from 'next' import * as path from 'path' const { createCanvas, registerFont, loadImage } = require('canvas') export default async (req: NextApiRequest, res: NextApiResponse) => { registerFont(path.resolve('./fonts/ipagp.ttf'), { family: 'ipagp', }) const width = 600 const height = 315 const canvas = createCanvas(width, height) const context = canvas.getContext('2d') context.fillStyle = '#fafafa' context.fillRect(0, 0, width, height) context.font = '15px ipagp' context.fillStyle = '#424242' context.textAlign = 'center' context.textBaseline = 'middle' context.fillText('あいうえお', 100, 50) const test = await loadImage(path.resolve('./images/test.png')) context.drawImage(test, 300, 0, 70, 70) const buffer = canvas.toBuffer() res.writeHead(200, { 'Cache-Control': 'public, max-age=315360000, s_maxage=315360000', Expires: new Date(Date.now() + 315360000000).toUTCString(), 'Content-Type': 'image/png', 'Content-Length': buffer.length, }) res.end(buffer, 'binary') } </code></pre> <p>下記がVercel上で描画した画像です。</p> <p><a href="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5f81c82e431f5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5f81c82e431f5.png?mw=700" alt="" /></a></p> <p>ちなみにnode-canvasでのOGPの作成はハマるのでそれは別途下記参照です。</p> <p><a href="https://crieit.net/posts/Vercel-Zeit-Now-Next-js-API-Routes-node-canvas">Vercel(元ZeitのNow)にてNext.jsのAPI Routesでnode-canvasを使う</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16070 2020-09-24T10:25:23+09:00 2020-09-24T10:25:23+09:00 https://crieit.net/posts/unlock-bank ハッカー専用パズルゲームを作ったので全てネタバレする <blockquote> <p>当記事は <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/kinmi/articles/b6646b4902dbda585c0b">ハッカー専用パズルゲームを作ったので全てネタバレする</a> のクロス投稿です</p> </blockquote> <h1 id="💰作ったもの💰"><a href="#%F0%9F%92%B0%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%F0%9F%92%B0">💰作ったもの💰</a></h1> <p><a href="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png?mw=700" alt="UNLOCK BANK" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://unlock-bank.vercel.app/">UNLOCK BANK</a></p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">@銀行口座をHackしたくてウズウズしている皆さん「違法行為はしたくないけどハッキングはしたい…」そんな貴方の悩みを解決するゲームをご用意しました。このゲームでは何をやっても合法です。是非、不正ログインしてみてください💰<a target="_blank" rel="nofollow noopener" href="https://t.co/LyIpYLCRrP">https://t.co/LyIpYLCRrP</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/UNLOCK_BANK?src=hash&ref_src=twsrc%5Etfw">#UNLOCK_BANK</a></p>— きんみ | ツイッター大喜利サイト🎍ついぎり🎍作りました🙄 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1307988548591611904?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote> <h2 id="仕様"><a href="#%E4%BB%95%E6%A7%98">仕様</a></h2> <ul> <li>Account Number(数字8桁) と Password(数字4桁) によるログイン認証</li> <li>Account Number は全て存在する(8桁なので全口座数は1億口)</li> <li>Password を3回間違えるとロック(再試行不可)される</li> <li><em>[裏設定]</em> 銀行口座を模している為、もちろん<strong>パスワードが流出したら他者もログイン可能</strong></li> </ul> <p>ユーザーはどんな攻撃手法を用いても良い。<br /> 最近話題の1段階認証を突破してみようというパズルゲーム。</p> <h2 id="システム構成"><a href="#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90">システム構成</a></h2> <ul> <li>JSフレームワーク: Nuxt.js 2.14.5(Vue.js 2.x)</li> <li>トランスパイル: TypeScript 4.0</li> <li>CSSフレームワーク: TailwindCSS</li> <li>ホスティング: Vercel</li> </ul> <p>お馴染みの構成を選択したが、Vue.jsの使用については少し後悔している(後述)</p> <h1 id="以下ネタバレ"><a href="#%E4%BB%A5%E4%B8%8B%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">以下ネタバレ</a></h1> <h2 id="想定する解法"><a href="#%E6%83%B3%E5%AE%9A%E3%81%99%E3%82%8B%E8%A7%A3%E6%B3%95">想定する解法</a></h2> <p>基本的には以下の2つと考えている。<br /> - <strong>☠️リバースブルートフォース攻撃☠️</strong><br /> 件の金融事件で脚光を浴びた攻撃手法。<br /> 暗証番号の総当たり(ブルートフォース攻撃)は一般的なシステムだと試行回数に制限を設けているが、ユーザーIDの様な公開情報については秘匿性が低いため無制限に試行できるシステムが多い。そこを突いてパスワードは固定のままIDの方を総当たりするという攻撃。<br /> 特に当ゲームのような、認証情報に数値しか許容していないシステムだと少ない試行回数で突破できるため致命的な脆弱性となる。</p> <ul> <li><strong>😈リバースエンジニアリング😈</strong><br /> JavaScriptのみで構成している為、コードを解析してパスワードの取得が可能。<br /> 当初はNode.jsサーバー等を用意して認証処理を隠蔽するつもりだったがガチ攻撃された場合、サーバーか私の財布が死ぬと思い断念。<br /> しかし「ハッカー専用」と銘打っている以上、コードを覗くだけで突破されては面白くないので悪足掻きとして難読化を施している。</li> </ul> <h2 id="仕組みについて"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">仕組みについて</a></h2> <h3 id="🔐パスワード生成"><a href="#%F0%9F%94%90%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E7%94%9F%E6%88%90">🔐パスワード生成</a></h3> <p>仕様として、口座番号とパスワードの紐付きを作らないといけない(パスワードが流出したら誰でもログインできるように)<br /> 最初は愚直に1億口座分のパスワードマッピングを定数で用意しようと考えた。<br /> しかし上述のコードを隠蔽できない事情によりリテラル値で持つのは避けたかった為、下記のライブラリを採用した。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom">seedrandom.js</a><br /> これを用いてシード値から再現性のある乱数を生成(※1)する。</p> <p>下記のように口座番号をシード値とした乱数を生成した後、整形してパスワードとしている。</p> <pre><code class="ts">const seedrandom = require('seedrandom') function authenticated(account: string, password: string): boolean { const rng = seedrandom(account) const _pass = String(Math.round(rng() * 10000)).padStart(4, '0') return _pass === password } </code></pre> <p>これで口座番号とパスワードの紐付きを再現している。</p> <h4 id="seedrandom.jsの使用感"><a href="#seedrandom.js%E3%81%AE%E4%BD%BF%E7%94%A8%E6%84%9F">seedrandom.jsの使用感</a></h4> <ul> <li>🙆‍♂️ <strong>良かった所</strong> <ul> <li>直感的に使える</li> <li>複数の乱数アルゴリズムに対応している</li> <li><code>Math.random()</code>をラップできる為、テストにも使えそう</li> </ul></li> <li>🙅‍♂️ <strong>頑張って〜</strong> <ul> <li>型定義がない(2020/09現在、<a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom/pull/70">issue</a>にて対応中)</li> </ul></li> </ul> <p>個人的には「再現性のある乱数を生成する処理」というのは<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/ddd213bf7a09f67f68ee">バーコードバトラーライクのゲーム</a>を作る際に重宝するので、有難いライブラリ。</p> <h3 id="状態管理, 認証処理"><a href="#%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%2C+%E8%AA%8D%E8%A8%BC%E5%87%A6%E7%90%86">状態管理, 認証処理</a></h3> <p>本筋と外れるが、クライアントで認証処理を行う為に口座番号と暗証番号をグローバル管理する必要があった。<br /> Vuexはオーバースペックなので状態を<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/guide/plugins/#%E7%B5%B1%E5%90%88%E3%81%95%E3%82%8C%E3%81%9F%E6%B3%A8%E5%85%A5">Inject</a>するプラグインを自作してstoreとして使用した。</p> <blockquote> <p>~/plugins/auth.ts</p> </blockquote> <pre><code class="ts">import Vue from 'vue' import { Plugin } from '@nuxt/types' import { Seedrandom } from '../types/seedrandom' const seedrandom = require('seedrandom') as Seedrandom type InjectTypeAuth = { /** * 口座番号 */ accountNumber: string /** * 暗証番号 */ password: string /** * 認証処理 * seedから4桁の乱数(先頭0埋め)を生成し、passwordとの比較結果を返却する * @param {string} seed シード値となる値 * @param {string} password パスワード */ authenticated(seed: string, password: string): boolean } declare module '@nuxt/types' { interface Context { $auth: InjectTypeAuth } interface NuxtAppOptions { $auth: InjectTypeAuth } } declare module 'vue/types/vue' { interface Vue { $auth: InjectTypeAuth } } type State = { accountNumber: string password: string } /********************************************** * 認証情報プラグイン * @param {Context} ctx * @param {(key: string, value: any) => void} inject */ const AuthPlugin: Plugin = (_ctx, inject) => { /** * Observable properties */ const state = Vue.observable({ accountNumber: '', password: '', } as State) function authenticated(seed: string, password: string): boolean { const rng = seedrandom(seed) const _pass = String(Math.round(rng() * 10000)).padStart(4, '0') if (_pass === password) state.password = _pass return _pass === password } /** * Injection */ inject('auth', { get accountNumber() { return state.accountNumber }, set accountNumber(accountNumber: string) { state.accountNumber = accountNumber }, get password() { return state.password }, authenticated, }) } export default AuthPlugin </code></pre> <p>※ 実際の乱数生成処理はもう少しノイズを入れてるので、このまま動かしても同じパスワードは生成されません</p> <p>この<code>authenticated</code>をEnterボタン押下時と、直アクセスを防ぐ目的でログイン後ページ内の<code>validate()</code>hookでも呼び出して認証処理としている。</p> <p>⚠️注意<br /> <em>当然ですが、フロントエンドでパスワードの一致チェック等の認証処理を行ってはいけません。</em></p> <p>Vue3のRCが外れて正式リリースとなったが、<a target="_blank" rel="nofollow noopener" href="https://v3.vuejs.org/api/basic-reactivity.html">Reactivity API</a>等を活用したVuex(ver 5)のリリースはまだ先の様なので暫く状態管理はこの手法に落ち着きそう。</p> <h2 id="攻撃方法の紹介"><a href="#%E6%94%BB%E6%92%83%E6%96%B9%E6%B3%95%E3%81%AE%E7%B4%B9%E4%BB%8B">攻撃方法の紹介</a></h2> <p>以上を踏まえ、解法の一つであるリバースブルートフォース攻撃の実施方法を紹介していく。<br /> 尚、当ゲームでは外部からブラウザ操作しやすいように主要な要素に対してid属性を付与している。<br /> <a href="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png?mw=700" alt="振っているID一覧" /></a></p> <h3 id="前提知識: JavaScript の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+JavaScript+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: JavaScript の場合</a></h3> <p>「<strong>システム構成</strong>」の節でも触れたが、Vue.jsの採用を後悔したのはここ。</p> <p>システムがHTML+PureJSの構成であれば、ブラウザの開発者コンソールから下記の様に実行して入力値を動的に与える事が可能。</p> <pre><code class="js">const digit1 = document.getElementById('digit1') digit1.value = "1" </code></pre> <p>これは一般的な方法だし、私も当初このやり方を想定していた。<br /> しかしVue.jsの場合、inputイベントを検知してVueインスタンス内に保有するデータを更新する。<br /> 直接input要素のvalue値を書き換えてもイベントは発火せず、データは更新されない。<br /> この挙動を想定しておらず、ユーザーに無駄なハードルを与えることとなってしまった。</p> <p>ではどうするのか。<br /> 当ゲームには数値をカウントアップ/カウントダウンするボタンを実装している。<br /> <a href="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png?mw=700" alt="これ" /></a></p> <p><strong>金融システムって誰得UI多いよな</strong> という遊び心で実装したボタンだが、Vue内のデータを書き換えるイベントに直結している。<br /> このボタンをプログラムからクリックすることが出来れば口座番号入力の自動化が可能となる。</p> <p>ここから先の解説は、既にUNLOCK成功された<a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoneapp">@yoneapp</a>さんが記事を書かれている為、そちらに任せることとする。<br /> <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/yoneapp/articles/ec2892c7e2e5c499684d">リバースブルートフォース攻撃を使ってUNLOCK BANKの口座に不正ログインして優勝する</a>(Zenn)<br /> (改めて解説記事の執筆ありがとうございます🙇‍♂️)</p> <h3 id="前提知識: Vue.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Vue.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Vue.js の場合</a></h3> <p>Vue.jsには <a target="_blank" rel="nofollow noopener" href="https://github.com/vuejs/vue-devtools">vue-devtools</a> というデバッグ用のブラウザ拡張が存在する。<br /> これを使えばコンポーネント構成を覗きつつ、各種データの書き換えやイベント発火が可能となるが通常はProduction環境で開けない。<br /> しかし、開発者ツールを使いこれをこじ開ける方法がある。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/POPOPON/items/60010faae1eb4e4e67ac">本番公開されているサイトで Vue devtools を使う裏技</a>(Qiita)</p> <p>厳密にはソースの難読化を施してるのため上記記事と全く同じではないが、ソースを検索すれば<code>devtool</code>というキーワードは見つかるはず。<br /> あとはその後ろにブレークポイントを張り、<code>_0x1bd719.devtools = true</code> を実行すれば開発者ツールを開けることが可能。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/y04xwot067stonsyjc0dxwtwrbj6" alt="ProductionでDevtoolsを開いた図" /></p> <p>図の状態(Devtoolsでコンポーネントを選択した状態)だと、開発者コンソール上で<code>$vm0</code>というオブジェクトが使用出来る。これは選択したコンポーネントのVueインスタンスであり、コンポーネント内に存在するデータやメソッドが全て内包されている。<br /> あとはそこからアタリを付けて、自動化プログラムを組めば良い。<br /> 実際には下記をイジれば入力操作の自動化が可能となる。</p> <pre><code class="js">$vm0.accountNumbers // 口座番号(1桁ずつ格納した配列) $vm0.password // パスワード $vm0.enter() // Enterボタン押下時の処理を実行 </code></pre> <p>⚠️注意<br /> <em>初めてこれ(Productionでdevtoolsが使えること)を知った方はセキュリティ面に不安を覚えるかもしれませんが、それはお門違いです。</em><br /> <em>フロントエンドで保持するデータはユーザーから自由に改ざんされる前提であるべきです。</em></p> <h3 id="前提知識: Nuxt.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Nuxt.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Nuxt.js の場合</a></h3> <p>Nuxt.jsで作られたアプリケーションは<code>window</code>直下に<code>$nuxt</code>というオブジェクトが作られる。<br /> そこには全ての情報が含まれており、上記のようにDevtoolsを開かずとも開発者コンソールからデータの書き換えやメソッドの実行が可能。<br /> しかし内包する情報量が膨大な為、開発者ではない第三者が操作したい対象を探すのは一苦労かもしれない。<br /> ちなみに当ゲームでは下記が自動化に必要な対象となる。</p> <pre><code class="js">window.$nuxt.$children[1].$children[0].$children[0].accountNumbers window.$nuxt.$children[1].$children[0].$children[0].password window.$nuxt.$children[1].$children[0].$children[0].enter() </code></pre> <h3 id="その他の手法"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E6%89%8B%E6%B3%95">その他の手法</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://www.selenium.dev/documentation/ja/">Selenium</a> や <a target="_blank" rel="nofollow noopener" href="https://github.com/puppeteer/puppeteer">Puppeteer</a> 等を用いたブラウザ操作の自動化があげられる。<br /> 今のところ、これらを使ってUNLOCKしたという報告は観測していない。ブラウザの開発者ツールが万能すぎる。</p> <p>⚠️注意<br /> <em>悪意が無くともサーバーに高負荷をかける行為は法律により罰せられる可能性があります。</em><br /> <em>しかし、当ゲームで行う分には問題ありません。存分に攻撃してください。</em></p> <h3 id="更にネタバレ"><a href="#%E6%9B%B4%E3%81%AB%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">更にネタバレ</a></h3> <p>もしproduction環境でログイン後のページを確認する必要が出た場合を考慮して、<br /> - <strong>Account Number</strong>: 1145-1419<br /> - <strong>Password</strong>: 1919</p> <p>と入力したら開発コンソールに上記口座のパスワードが出力されるようになっている。<br /> 攻撃するのは面倒くさいけどログインしてみたい、という方はどうぞ。</p> <h2 id="確率について"><a href="#%E7%A2%BA%E7%8E%87%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">確率について</a></h2> <p>数値4桁のパスワードというのは 0000 ~ 9999 の1万パターンしかない。<br /> パスワードを固定にして、口座番号を総当たりした場合の確率は下記で求められる。</p> <p><em>1 - ( 9999 / 10000 )^x</em><br /> x=試行回数</p> <p>(間違ってたらご指摘ください)<br /> 確率を表にするとこうなる。<br /> <div class="table-responsive"><table> <thead> <tr> <th>試行回数</th> <th>HITする確率</th> </tr> </thead> <tbody> <tr> <td>3回</td> <td>0.03%未満</td> </tr> <tr> <td>10回</td> <td>約0.1%</td> </tr> <tr> <td>100回</td> <td>約1%</td> </tr> <tr> <td>1,000回</td> <td>約10%</td> </tr> <tr> <td>10,000回</td> <td>約63%</td> </tr> <tr> <td>100,000回</td> <td>約99%</td> </tr> </tbody> </table></div></p> <p>つまり、口座番号8桁(1億パターン)全て試行せずとも、10万回程度で1口座はUNLOCKできてしまう。<br /> 下5桁総当たりすればいい計算で、実際にTwitterでUNLOCKされた方の反応も「思ったより早かった」という声が多かった。<br /> 当ゲームはパスワードを乱数によって生成しているが、これが本当の銀行口座の場合、パスワードの偏りが生じるはずなので更にUNLOCKは容易となるだろう。</p> <h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1> <p>ネタのつもりで作ったんですが思いのほか反響があって、解説記事を書いて頂いたり、難読化をデコードしてリバースエンジニアリングまでやって下さってる方もいて、個人開発冥利に尽きるなと思いました。<br /> 他にも「こんなやり方あるよ」という方がいましたらご連絡ください。</p> <h1 id="注釈"><a href="#%E6%B3%A8%E9%87%88">注釈</a></h1> <p>※1: JavaScriptにはシード値から乱数を生成する機能が備わっていない</p> きんみ tag:crieit.net,2005:PublicArticle/16034 2020-08-18T23:18:26+09:00 2020-08-19T20:34:19+09:00 https://crieit.net/posts/Crieit-5f3be332191fb 【アップデート】Crieitバッジを作りました <h1 id="Crieitバッジ"><a href="#Crieit%E3%83%90%E3%83%83%E3%82%B8">Crieitバッジ</a></h1> <p>GitHubなどに貼るCrieitバッジを作りました。</p> <p>これが私(ckoshien)のバッジです。<br /> <a target="_blank" rel="nofollow noopener" href="http://crieit.net/users/ckoshien"><img src="https://ogp-vercel.vercel.app/crieit/ckoshien" alt="Crieit経験値" /></a></p> <p>ちなみにCrieit開発者だらさんはといえば....<br /> <a target="_blank" rel="nofollow noopener" href="http://crieit.net/users/dala00"><img src="https://ogp-vercel.vercel.app/crieit/dala00" alt="Crieit経験値" /></a></p> <p>....やっぱり強い強すぎる。</p> <h2 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h2> <p>このコードの*****の部分を自分のユーザID(例:ckoshienとかdala00)に書き換えて貼るだけです。</p> <pre><code>[![Crieit経験値](https://ogp-vercel.vercel.app/crieit/*****)](http://crieit.net/users/*****) </code></pre> <h1 id="どうやって作ったか"><a href="#%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E4%BD%9C%E3%81%A3%E3%81%9F%E3%81%8B">どうやって作ったか</a></h1> <h2 id="着想"><a href="#%E7%9D%80%E6%83%B3">着想</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mikkame/mikkame">mikkame</a>さんの<a target="_blank" rel="nofollow noopener" href="https://qiita-badge.apiapi.app/">Qiita のスコアをGithub風バッジに変換するサービス</a>を見てCrieit版作ろう!と思いました。</p> <h2 id="技術的なこと"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E3%81%93%E3%81%A8">技術的なこと</a></h2> <p>shield.io版からアップデートしました。<br /> 現在はvercel + NextJS + puppeteerでスクレイピングした情報で一旦画面を作ってスクリーンショットを撮っています。</p> <p>結果的にvercelを2台分(スクレイピング/スクリーンショット)使っています。</p> <h3 id="バックエンド"><a href="#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89">バックエンド</a></h3> <ul> <li>vercel/NodeJS(lambda)</li> <li>puppeteer</li> </ul> <p>puppeteerでcrieitのユーザページをスクレイピングして経験値を取得しています。</p> <pre><code class="javascript">async function getCrieitBadge(user_id) { const browser = await puppeteer.launch({ args: chrome.args, executablePath: await chrome.executablePath, headless: chrome.headless, }); const page = await browser.newPage(); await page.goto("https://crieit.net/users/"+user_id); await page.setCacheEnabled(true); const itemSelector = "#app > #container > div.row > div > p.mb-2 > span:nth-child(1)"; const itemSelector2 = "#app > #container > div.row > div > p.mb-2 > span:nth-child(2)"; var item = await page.$(itemSelector); var item2 = await page.$(itemSelector2); var data = await (await item.getProperty('textContent')).jsonValue(); var data2 = await (await item2.getProperty('textContent')).jsonValue(); await page.close(); await browser.close(); return data; } </code></pre> <p>現在は画像を1日キャッシュするようにして返しています。<br /> ではよいCrieitライフを!!</p> ckoshien tag:crieit.net,2005:PublicArticle/15876 2020-04-27T23:20:13+09:00 2020-04-27T23:20:13+09:00 https://crieit.net/posts/Vercel-Zeit-Now-Nuxt-js-2020 Vercel(元ZeitのNow)でNuxt.jsアプリをデプロイ 2020年最新版 <p><code>now</code> というコマンドを使わないVercel(元ZeitのNow)へのデプロイ方法を解説します。今回の手順ではNuxt.jsを使います。</p> <h2 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h2> <p><code>now</code> というコマンドで簡単にデプロイできるということで一世を風靡したZeitのNowですが、先日Vercelというサービス名に変更されました。その前からちょこちょこ新たな機能をリリースしており、nowというコマンドを使わなくてもGitHub連携によってpushするだけでデプロイできるようになったり、環境変数も独自のsecretsという設定を使わず、管理画面で設定できるようになったりと、かなり手軽になってきているので実際にそれらを使ったデプロイ方法を説明します。</p> <h2 id="Nuxt.jsアプリを作成"><a href="#Nuxt.js%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E6%88%90">Nuxt.jsアプリを作成</a></h2> <p>まずはNuxt.jsのアプリケーションを作成します。</p> <pre><code>npx create-nuxt-app nuxt-vercel-sample # or yarn create nuxt-app nuxt-vercel-sample </code></pre> <p>いくつか設定項目が現れるため下記のように設定しました。適当です。</p> <pre><code class="text">✨ Generating Nuxt.js project in nuxt-vercel-sample ? Project name nuxt-vercel-sample ? Project description My astonishing Nuxt.js project ? Author name dala00 ? Choose programming language TypeScript ? Choose the package manager Yarn ? Choose UI framework Buefy ? Choose custom server framework None (Recommended) ? Choose the runtime for TypeScript Default ? Choose Nuxt.js modules Axios, Progressive Web App (PWA) Support, DotEnv ? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose test framework Jest ? Choose rendering mode Universal (SSR) ? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection) </code></pre> <p>あとは出力されるメッセージの通り、下記のようにして実行できます。</p> <pre><code>cd nuxt-vercel-sample yarn dev </code></pre> <p>見た目は利用しているUIフレームワークによって変わります。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e12b13314.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e12b13314.png?mw=700" alt="" /></a></p> <h2 id="GitHubへ登録"><a href="#GitHub%E3%81%B8%E7%99%BB%E9%8C%B2">GitHubへ登録</a></h2> <p>今回はnowコマンドは一切使わずGitHub連携でデプロイを行います。ですのでまずはいま作成したアプリケーションをGitHubに登録します。その前にとりあえずcommitしておきます。<code>git init</code>だけは勝手にやってくれています。</p> <pre><code>git add . git commit -m "Initial commit" </code></pre> <p>VSCode等でやってもよいでしょう。</p> <p>まずは適当にGitHubでリポジトリを作成します。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e189f259c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e189f259c.png?mw=700" alt="" /></a></p> <p>表示されているとおりに設定してpushします(SSHキーの登録などは事前に行っておいてください)。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e1ac15622.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e1ac15622.png?mw=700" alt="" /></a></p> <h2 id="Vercelにデプロイする"><a href="#Vercel%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B">Vercelにデプロイする</a></h2> <p>続いてVercel側に今pushしたものをデプロイします。……がその前に、Nuxt.jsをVercelで使うための <code>@nuxtjs/now-builder</code> というものがNuxt.js公式から用意されていますので、それをnow.jsonに設定します。</p> <pre><code class="json">{ "version": 2, "builds": [ { "src": "nuxt.config.js", "use": "@nuxtjs/now-builder", "config": {} } ] } </code></pre> <p>これをcommit & pushしておきましょう。</p> <p>続いてVercelのダッシュボードにImport Projectボタンがあるためそちらをクリックします。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459d4063ee.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459d4063ee.png?mw=700" alt="" /></a></p> <p>From Git Repositoryを選択します。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459fd1c53f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459fd1c53f.png?mw=700" alt="" /></a></p> <p>Importボタンをクリックします。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45a3fb370b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45a3fb370b.png?mw=700" alt="" /></a></p> <p>プロジェクトの選択画面が開きますので適宜選択します。僕の場合は個別に権限を与えたものだけ選択できるようにしているため、同様の場合は下記のようにEdit your repository access settings on GitHub.を実行してGitHub側で権限を与えてから選択します。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45abb2e10f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45abb2e10f.png?mw=700" alt="" /></a></p> <p>選択できたらSelectしてImportを実行です。プロジェクト名の入力画面が開くので入力してContinueします。</p> <p>引き続きROOT DIRECTORYやBUILD COMMANDの設定画面なども開きますが、Nuxt.jsアプリケーションの場合は一切何も設定しなくてもデプロイが可能のためひたすらContinueして最後のDeployボタンを押せば完了です(さきほどのnow.jsonの設定が抜けているとビルドに失敗します)。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e54ca8f75.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e54ca8f75.png?mw=700" alt="" /></a></p> <p>Buildingステータスになっているのでしばし待ちます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bdf2d264.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bdf2d264.png?mw=700" alt="" /></a></p> <p>Readyとなったらデプロイ完了です。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e59b5216c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e59b5216c.png?mw=700" alt="" /></a></p> <p>URLがいくつかありますが、どれでも閲覧できます。DEPLOYMENTというURLは一意なデプロイURLです。DOMAINSはそのプロジェクトのURLで、いつも使えるURLです。どれでも良いので開くとローカルで実行したアプリケーションと同じ画面が表示されます。これでデプロイは完了です!</p> <p>ちなみに下記は今回作ったサンプルです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://nuxt-vercel-sample.now.sh/">https://nuxt-vercel-sample.now.sh/</a><br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/nuxt-vercel-sample">https://github.com/dala00/nuxt-vercel-sample</a></p> <h2 id="更新したい時"><a href="#%E6%9B%B4%E6%96%B0%E3%81%97%E3%81%9F%E3%81%84%E6%99%82">更新したい時</a></h2> <p>プログラムを調整して更新したいときは、GitHubにpushするだけで勝手にデプロイされます。</p> <p>ちなみに先ほどの画面はProduction Deploymentで、masterブランチのものがProductionとしてデプロイされます。別のブランチを作ってそちらでpushしておくと、その製品版のURLは利用されず、別ブランチ用のURLが作成されるため安全にステージング環境として利用することが可能です。下記のようにDeploymentsメニューで全てのブランチのデプロイを確認できます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e68aa6335.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e68aa6335.png?mw=700" alt="" /></a></p> <p>それをクリックすると下記のようにテスト用のURLを利用することが出来ます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e6e256a15.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e6e256a15.png?mw=700" alt="" /></a></p> <p>プルリクエストを作るとブランチのマージもGitHub上だけで行えます。</p> <p>ちなみにGitHub連携しているとこんな感じでnowちゃんが甲斐甲斐しくコメントをしてくれます(そういやこちらはまだ名前が変わってないですね)。これで一人ぼっちで友達がいなくても寂しくありません。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e70fa5c7a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e70fa5c7a.png?mw=700" alt="" /></a></p> <h2 id="環境変数を設定する"><a href="#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B">環境変数を設定する</a></h2> <p>Nowではずっとsecretsという機能を利用してプライベートな値を利用できるようにしていましたが、最近Vercelになったのと同じくらいの時期に管理画面で直接環境変数を設定できるようになりました。下記のように設定ができます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594c111ec1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594c111ec1.png?mw=700" alt="" /></a></p> <p>ちなみにProductionは本番、つまり独自ドメインを設定していればそちらのURLや、<code>プロジェクト名.now.sh</code> というシンプルなエイリアスのものなどです。Previewは別のブランチでpushした時に作成されるURLです。DevelopmentはローカルPCにて、<code>now dev</code>を実行した時のものです。</p> <p>設定後は見えなくなります。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594dc71d71.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594dc71d71.png?mw=700" alt="" /></a></p> <p>Nuxt.js上では、nuxt.config.jsにて例えば下記のように設定します。ローカルでの環境変数は直接指定するか、dotenv等を利用しましょう。</p> <pre><code class="javascript">// require('dotenv').config() export default { mode: "universal", env: { TEST_TEXT: process.env.TEST_TEXT }, </code></pre> <p>index.vueにてscript側に値を取得処理を作成し、</p> <pre><code class="javascript"> methods: { getText() { return process.env.TEST_TEXT } } </code></pre> <p>表示側でそれを使います。</p> <pre><code class="html"> <span>{</span><span>{</span> getText() <span>}</span><span>}</span> </code></pre> <p>問題なければ下記のように表示されます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e9d8ecc0e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea6e9d8ecc0e.png?mw=700" alt="" /></a></p> <h2 id="nowコマンドは?"><a href="#now%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AF%EF%BC%9F">nowコマンドは?</a></h2> <p>こちらも使えます。今回のパターンだと不要でしたが、 複数のbuildersを使う場合などはローカルで <code>now dev</code> コマンドで開発ができます。この場合は事前に <code>now</code> コマンドを実行してVercel側のプロジェクトと紐付けておく必要があります。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>ということで2020年に入り、Vercelは進化を遂げ続けており非常に使いやすくなりました。GitHubや自動デプロイに慣れている方はGitHub連携で、GitHubを使ったことがなくてちょっと怖い、という方は今まで通りnowコマンドを使って、柔軟に利用できます。非常に便利ですので使い倒していきましょう。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15871 2020-04-27T08:55:36+09:00 2020-04-27T08:55:36+09:00 https://crieit.net/posts/Vercel-Zeit-Now-Next-js-2020 Vercel(元ZeitのNow)でNext.jsアプリをデプロイ 2020年最新版 <p><code>now</code> というコマンドを使わないVercel(元ZeitのNow)へのデプロイ方法を解説します。今回の手順ではNext.jsを使います。</p> <h2 id="背景"><a href="#%E8%83%8C%E6%99%AF">背景</a></h2> <p><code>now</code> というコマンドで簡単にデプロイできるということで一世を風靡したZeitのNowですが、先日Vercelというサービス名に変更されました。その前からちょこちょこ新たな機能をリリースしており、nowというコマンドを使わなくてもGitHub連携によってpushするだけでデプロイできるようになったり、環境変数も独自のsecretsという設定を使わず、管理画面で設定できるようになったりと、かなり手軽になってきているので実際にそれらを使ったデプロイ方法を説明します。</p> <h2 id="Next.jsアプリを作成"><a href="#Next.js%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E6%88%90">Next.jsアプリを作成</a></h2> <p>まずはNext.jsのアプリケーションを作成します。</p> <pre><code>npm init next-app # or yarn create next-app </code></pre> <p>いくつか設定項目が現れるため下記のように設定しました。</p> <pre><code class="text">√ What is your project named? ... next-vercel-sample √ Pick a template » Default starter app </code></pre> <p>あとは出力されるメッセージの通り、下記のようにして実行できます。</p> <pre><code>cd next-vercel-sample yarn dev </code></pre> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea457cd94de6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea457cd94de6.png?mw=700" alt="" /></a></p> <h2 id="GitHubへ登録"><a href="#GitHub%E3%81%B8%E7%99%BB%E9%8C%B2">GitHubへ登録</a></h2> <p>今回はnowコマンドは一切使わずGitHub連携でデプロイを行います。ですのでまずはいま作成したアプリケーションをGitHubに登録します。その前にとりあえずcommitしておきます。</p> <pre><code>git init git add . git commit -m "Initial commit" </code></pre> <p>addとcommitはVSCode等でやってもよいでしょう。</p> <p>まずは適当にGitHubでリポジトリを作成します。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea458e4a52d5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea458e4a52d5.png?mw=700" alt="" /></a></p> <p>表示されているとおりに設定してpushします(SSHキーの登録などは事前に行っておいてください)。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea4591709f04.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea4591709f04.png?mw=700" alt="image.png" /></a></p> <h2 id="Vercelにデプロイする"><a href="#Vercel%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B">Vercelにデプロイする</a></h2> <p>続いてVercel側に今pushしたものをデプロイします。VercelのダッシュボードにImport Projectボタンがあるためそちらをクリックします。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459d4063ee.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459d4063ee.png?mw=700" alt="" /></a></p> <p>From Git Repositoryを選択します。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459fd1c53f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea459fd1c53f.png?mw=700" alt="" /></a></p> <p>Importボタンをクリックします。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45a3fb370b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45a3fb370b.png?mw=700" alt="" /></a></p> <p>プロジェクトの選択画面が開きますので適宜選択します。僕の場合は個別に権限を与えたものだけ選択できるようにしているため、同様の場合は下記のようにEdit your repository access settings on GitHub.を実行してGitHub側で権限を与えてから選択します。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45abb2e10f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45abb2e10f.png?mw=700" alt="" /></a></p> <p>選択できたらSelectしてImportを実行です。プロジェクト名の入力画面が開くので入力してContinueします。</p> <p>引き続きROOT DIRECTORYやBUILD COMMANDの設定画面なども開きますが、Next.jsアプリケーションの場合は一切何も設定しなくてもデプロイが可能のためひたすらContinueして最後のDeployボタンを押せば完了です。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45ba06d7ee.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45ba06d7ee.png?mw=700" alt="" /></a></p> <p>Buildingステータスになっているのでしばし待ちます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bdf2d264.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bdf2d264.png?mw=700" alt="" /></a></p> <p>Readyとなったらデプロイ完了です。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bfc2da35.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45bfc2da35.png?mw=700" alt="" /></a></p> <p>URLがいくつかありますが、どれでも閲覧できます。DEPLOYMENTというURLは一意なデプロイURLです。DOMAINSはそのプロジェクトのURLで、いつも使えるURLです。どれでも良いので開くとローカルで実行したアプリケーションと同じ画面が表示されます。これでデプロイは完了です!</p> <p>ちなみに下記は今回作ったサンプルです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://next-vercel-sample.now.sh/">https://next-vercel-sample.now.sh/</a><br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/next-vercel-sample">https://github.com/dala00/next-vercel-sample</a></p> <h2 id="更新したい時"><a href="#%E6%9B%B4%E6%96%B0%E3%81%97%E3%81%9F%E3%81%84%E6%99%82">更新したい時</a></h2> <p>プログラムを調整して更新したいときは、GitHubにpushするだけで勝手にデプロイされます。</p> <p>ちなみに先ほどの画面はProduction Deploymentで、masterブランチのものがProductionとしてデプロイされます。別のブランチを作ってそちらでpushしておくと、その製品版のURLは利用されず、別ブランチ用のURLが作成されるため安全にステージング環境として利用することが可能です。下記のようにDeploymentsメニューで全てのブランチのデプロイを確認できます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45dd338c3e.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45dd338c3e.png?mw=700" alt="" /></a></p> <p>それをクリックすると下記のようにテスト用のURLを利用することが出来ます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45e23b9385.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea45e23b9385.png?mw=700" alt="" /></a></p> <p>プルリクエストを作るとブランチのマージもGitHub上だけで行えます。</p> <p>ちなみにGitHub連携しているとこんな感じでnowちゃんが甲斐甲斐しくコメントをしてくれます(そういやこちらはまだ名前が変わってないですね)。これで一人ぼっちで友達がいなくても寂しくありません。</p> <h2 id="環境変数を設定する"><a href="#%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B">環境変数を設定する</a></h2> <p>Nowではずっとsecretsという機能を利用してプライベートな値を利用できるようにしていましたが、最近Vercelになったのと同じくらいの時期に管理画面で直接環境変数を設定できるようになりました。下記のように設定ができます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594c111ec1.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594c111ec1.png?mw=700" alt="" /></a></p> <p>ちなみにProductionは本番、つまり独自ドメインを設定していればそちらのURLや、<code>プロジェクト名.now.sh</code> というシンプルなエイリアスのものなどです。Previewは別のブランチでpushした時に作成されるURLです。DevelopmentはローカルPCにて、<code>now dev</code>を実行した時のものです。</p> <p>設定後は見えなくなります。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594dc71d71.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea594dc71d71.png?mw=700" alt="" /></a></p> <p>Next.js上では、next.config.jsにて例えば下記のように設定します。ローカルでの環境変数は直接指定するか、dotenv等を利用しましょう。envはサーバー側(というかビルド時)、publicRuntimeConfigでクライアント側に環境変数を渡せます(漏洩してはいけない値を渡さないようにしましょう)。</p> <pre><code class="javascript">// require('dotenv').config() module.exports = { env: { TEST_TEXT: process.env.TEST_TEXT, }, publicRuntimeConfig: { TEST_TEXT: process.env.TEST_TEXT, }, } </code></pre> <p>index.jsを下記のように調整することで利用できます。</p> <pre><code class="jsx">import getConfig from 'next/config' const { publicRuntimeConfig } = getConfig() export default function Home() { return ( <div className="container"> <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> <main> <h1 className="title"> Welcome to{' '} <a href="https://nextjs.org"> Next.js! {publicRuntimeConfig.TEST_TEXT} </a> </h1> </code></pre> <p>問題なければ下記のように表示されます。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea596b34af76.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5ea596b34af76.png?mw=700" alt="" /></a></p> <h2 id="nowコマンドは?"><a href="#now%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AF%EF%BC%9F">nowコマンドは?</a></h2> <p>こちらも使えます。今回のパターンだと不要でしたが、 <code>@now/next</code> というbuilders以外を使う場合はローカルで <code>now dev</code> コマンドで開発ができます。この場合は事前に <code>now</code> コマンドを実行してVercel側のプロジェクトと紐付けておく必要があります。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>ということで2020年に入り、Vercelは進化を遂げ続けており非常に使いやすくなりました。GitHubや自動デプロイに慣れている方はGitHub連携で、GitHubを使ったことがなくてちょっと怖い、という方は今まで通りnowコマンドを使って、柔軟に利用できます。非常に便利ですので使い倒していきましょう。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15865 2020-04-24T12:00:32+09:00 2020-10-11T00:03:51+09:00 https://crieit.net/posts/Vercel-Zeit-Now-Next-js-node-canvas-OGP Vercel(元ZeitのNow)にてNext.jsでnode-canvasを使ってOGP <p>追記 2020/10)<br /> 2020年7月頃から?、Vercelが改善されて簡単にOGPが作れるようになりました。<br /> <a href="https://crieit.net/posts/Vercel-API-Routes-OGP-2020">VercelのAPI RoutesでOGPを作成する 2020年版</a><br /> 追記終わり</p> <p>Next.jsアプリケーションをVercel(元ZeitのNow)で利用してOGPを生成しようと思ったが無理で、それでもなんとか成功させたある男の記録(2020/4、Next.js9.3現在)。</p> <h2 id="VercelでNext.js"><a href="#Vercel%E3%81%A7Next.js">VercelでNext.js</a></h2> <p>そもそもNext.jsはVercelの人が作っている。そのため多分かなり親和性が高い。GitHub連携して適当にpushすればすぐなんの設定もなくすぐデプロイできる。ということでまずは何も考えずガリガリアプリケーションを作成していた。</p> <h2 id="何がうまくいかなかったのか"><a href="#%E4%BD%95%E3%81%8C%E3%81%86%E3%81%BE%E3%81%8F%E3%81%84%E3%81%8B%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%AE%E3%81%8B">何がうまくいかなかったのか</a></h2> <p>まず作っていたもの。それは普通のNext.jsアプリケーション。それにOGPもつけようと思っていた。調べてみるとNext.jsには <a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-routes/introduction">API Routes</a> という機能があり、別途サーバー用の機能を準備しなくてもちょっとしたAPIならpages/apiの下に通常のページと同じ配置でメソッドを追加すればサーバーサイドの処理も書けてしまう。</p> <p>これでOPGも可能そうだと思い、事前にnode-canvasを使って試していたところうまくいっていた。前も別のところで同じことをやっていたのだが、基本的にこういうデプロイ先はフォントが無いことがあるため、node-canvasにあるregisterFontという機能を使って直接ttfのようなフォントファイルを読み込むことで文字も描画できるようにしている。</p> <p>しかし、これがうまくいかなかった。なんかregisterFontあたりエラーが出る。調べてみたところこんなissueをみつけた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/zeit/next.js/issues/8251">Next.js API routes (and pages) should support reading files · Issue #8251 · zeit/next.js</a></p> <p>そう、なんか <code>__dirname</code> もちゃんと機能していないし、<code>fs.readFileSync</code> もまともに動かない……。</p> <p>ということで、フォントの読み込みができなかった。</p> <h2 id="なぜファイルを読み込めないのか"><a href="#%E3%81%AA%E3%81%9C%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%81%E3%81%AA%E3%81%84%E3%81%AE%E3%81%8B">なぜファイルを読み込めないのか</a></h2> <p>なぜかはよくわからないが、とにかくVercelは内部的にはAWSのLambdaを使っており、その関係でデプロイする時に関係ないファイルを無視していたり、フォルダ構成も全く違う感じになってたり、色んな要因でうまくいかないのではないかと思われる。詳しくはわからない。が、上記issueやこのPR <a target="_blank" rel="nofollow noopener" href="https://github.com/zeit/next.js/pull/8334">Fix for __dirname by huv1k · Pull Request #8334 · zeit/next.js</a> を見る限り、なかなか改善できていないようなので多分構成的にそんな簡単ではないのではないかと思われる。</p> <p>ということで、そのうち改善されるのかもしれないが、とにかく今や今後すぐに改善されるとは思わないほうが良いっぽく、無理なら無理でどうするかを考える必要がありそうだった。</p> <h2 id="解決の手がかり"><a href="#%E8%A7%A3%E6%B1%BA%E3%81%AE%E6%89%8B%E3%81%8C%E3%81%8B%E3%82%8A">解決の手がかり</a></h2> <p>実はこのissueの中に、下記のようなコメントがあった。結果としてはこれが解決方法。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/zeit/next.js/issues/8251#issuecomment-614220305">https://github.com/zeit/next.js/issues/8251#issuecomment-614220305</a></p> <p>具体的には <code>@now/next</code> と <code>@now/node</code> のbuildersを併用するというもの。</p> <h3 id="buildersとは"><a href="#builders%E3%81%A8%E3%81%AF">buildersとは</a></h3> <p>Vercelにはbuildersという概念があり、これによりどの様にアプリケーションをデプロイするかを決めることができる。 @now/next はNext.jsをデプロイする時に自動的に選択される、Next.js用のbuilders。 @now/nodeはそれ以外のNode.jsアプリケーション。</p> <h3 id="なぜ併用するのか"><a href="#%E3%81%AA%E3%81%9C%E4%BD%B5%E7%94%A8%E3%81%99%E3%82%8B%E3%81%AE%E3%81%8B">なぜ併用するのか</a></h3> <p>ではなぜ併用する必要があるのかと言うと、先程のissueにも書かれているのだが、実はincludeFilesという設定があり、これを利用すると指定したファイルをデプロイ時に無視しないようにし、正常にデプロイ先で読み込むことができるようになる。</p> <p>ところが、これが現在 @now/next では利用できない。そのため、 @now/node を併用して利用することでOPGの生成のようにファイルを読み込む日強グアある処理だけはそちらで稼働させようという考え。</p> <h2 id="実際に解決した方法"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E8%A7%A3%E6%B1%BA%E3%81%97%E3%81%9F%E6%96%B9%E6%B3%95">実際に解決した方法</a></h2> <p>まず、<code>pages/api</code>にあった処理を <code>functions/index.ts</code> に移した。Next.js側では使えないため、API Routesも使えない。そのため Next.jsの外に出して別で稼働させる。apiというフォルダに入れれば @now/node で <code>/api/~~</code> というURLで実行できるのだが、これはNext.jsのAPI Routesと被ってしまうので今回はとりあえず <code>/functions/~~</code> というURLでアクセスできるようにした。具体的なnow.jsonの設定は下記の通り(最近はGitHub経由でデプロイできるし、独自ドメインや環境変数も管理画面で設定できるのでそもそもnow.json自体なく、今回追加したこれで全部)。</p> <pre><code>{ "builds": [ { "src": "next.config.js", "use": "@now/next" }, { "src": "functions/index.ts", "use": "@now/node", "config": { "includeFiles": ["fonts/*"] } } ], "routes": [ { "src": "/functions/(.+)", "dest": "/functions/index.ts" } ] } </code></pre> <p>これでfontsというフォントを入れたフォルダを維持したまま、functionsで始まるURLで @now/node ビルドのサーバーサイド処理にアクセスすることができる。</p> <p>で、色々端折るけどfunctions/index.tsはこんな感じ。</p> <pre><code class="typescript">import * as path from 'path' const { createCanvas, registerFont } = require('canvas') import { NowRequest, NowResponse } from '@now/node' export default function (req: NowRequest, res: NowResponse) { registerFont(path.join('fonts', 'myfont.ttf'), { family: 'myfont', }) const canvas = createCanvas(600, 315) const context = canvas.getContext('2d') context.font = '15px myfont' context.fillStyle = '#424242' context.fillText('hello', 100, 100) const image = canvas.toBuffer() res.writeHead(200, { 'Content-Type': 'image/png', 'Content-Length': image.length, }) res.end(image, 'binary') } </code></pre> <p>ちなみに、あくまでも @now/next と @now/node は別環境のため、フォントをアップできたからといってNext.js側でそれを利用することは出来ない。今回はOGPだったためURLだけで連携できるのでOKだった。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>このように、一時期は別サーバーで対応が必要かとも考えたが、なんとかVercelだけで全てを完結させることができたので本当に良かった。Vercel最高。だけどはやくファイルの読み込み関連はいい感じに使えるように改善して欲しいものだ。</p> <p>ちなみに実際に作ったのはこれ(石を進めたときだけOGPが出る)</p> <p><a target="_blank" rel="nofollow noopener" href="https://reversi-game-record.appllis.net/">リバーシ棋譜ツール</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15861 2020-04-22T09:09:44+09:00 2020-10-13T11:10:01+09:00 https://crieit.net/posts/Vercel-Zeit-Now-Next-js-API-Routes-node-canvas Vercel(元ZeitのNow)にてNext.jsのAPI Routesでnode-canvasを使う <p>Next.jsにはAPI Routesという機能があり、<code>pages/api</code> 配下に通常のページと同じように配置してサーバーサイドの処理を書くことができる。アクセスは <code>/api/パス</code> のような感じ。</p> <pre><code class="javascript">export default (req, res) => { res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify({ name: 'John Doe' })) } </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-routes/introduction">https://nextjs.org/docs/api-routes/introduction</a></p> <p>この機能を使ってOGPを実装し、いざVercelにデプロイしてみるとなぜか動かない。ローカルではうまくいったのに。下記のようなエラーが出る。</p> <pre><code class="text">Error: libuuid.so.1: cannot open shared object file: No such file or directory </code></pre> <p>GitHubにも同じ現象についてのissueがあり、解決法を書いている人もいた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/zeit/now/issues/3460#issuecomment-574712112">https://github.com/zeit/now/issues/3460#issuecomment-574712112</a></p> <p>ちょっと分かりづらいのでもうちょっと詳しく解説。</p> <pre><code class="sh">wget https://github.com/jwerre/node-canvas-lambda/raw/master/node12_canvas_lib64_layer.zip unzip -j -d canvas_lib64 node12_canvas_lib64_layer.zip </code></pre> <p>上記はローカルで実行する。そして出来上がったcanvas_lib64フォルダをコミットしてしまう(now-buildコマンドに混ぜてみたがwgetが無いとどやされた)。</p> <p>ちなみに上記のzipファイルの配置が時々変わるようなので、その場合は該当のリポジトリを確認。</p> <pre><code class="json">{ "scripts": { "now-build": "cp canvas_lib64/*so.1 node_modules/canvas/build/Release/" } } </code></pre> <p>上記は自分のpackage.jsonのscriptsの中にこのnow-buildコマンドを追加するという意味。ただ、これだとうまくいかなかったため下記にした。多分コピーだけになってしまってビルドされなくなってしまったのではないかと思う。</p> <pre><code class="sh">cp canvas_lib64/*so.1 node_modules/canvas/build/Release/ && yarn build </code></pre> <p>これで動いた。</p> <p>ただし、VercelにてNext.jsのAPI Routesでは、なんと他のファイルを読み込むことが出来ない。フォントも画像も。そのためこのままだとひたすら描画して作るしかない。</p> <p>対処法は <code>@now/next</code> と <code>@now/node</code> のbuildersを併用するのだが詳しくは後日別の記事にて。</p> <p>↓後日の別の記事<br /> <a href="https://crieit.net/posts/Vercel-Zeit-Now-Next-js-node-canvas-OGP">Vercel(元ZeitのNow)にてNext.jsでnode-canvasを使ってOGP</a></p> だら@Crieit開発者