ジャバ・ザ・ハットリ
Published on

Gatsbyにログイン機能をつけてみよう

Authors
  • avatar
    ジャバ・ザ・ハットリ

Gatsby は実はカンタンにログイン機能をつけることができる。ユーザーがシステムにログインして固有の情報を見せる機能。例えばジャスティン氏がログインしたら「ジャスティン・ビーバーさん、今日もあたしの Gatsby にログインしてくれてありがとう(ハート)」みたいなメッセージを出すとか。

Gatsby の爆速に惚れて「なんか Gatsby で作ろうかな」となったとする。用途はブログでも創作物の作品集でもランディングページでもいい。「ログイン機能を付けたいんだけどなー。でもこれは『静的サイト』だろ。無理かな?」と考えてしまっていたら「それカンタンにできますよ」と言いたい。フルカスタマイズが可能な Gatsby の場合、だいたいなんでもできる。

もろろん会員情報を入れておくためのサーバーとして Firebase や Rails が必要にはなる。表側の爆速処理は全部 Gatsby にやってもらって、裏側の会員情報やらをサーバーにやってもらう、となる。

ブログのようにほとんどの情報出力が静的な内容で、たまーにユーザーログインが要る、ってぐらいならこれで十分。

Gatsby には豊富はプラグインがあるので、だいたいの機能追加にはプラグインをインストールすれば1発で終わる。でもこのログイン機能にはプラグインが無い。それはサーバー側の構成によって変わるし、プラグインの作りようが無いからだと思う。

ただそんなに難しくもないのでこの解説を読んで試してみてはいかがでしょうか?

ここではサーバー側は無しであくまで Gatsby の設定だけを書いた。

この解説で完成するログイン機能ありのブログはこちら。https://jabba14.gitlab.io/gatsby_login/

image

ソースコードはこちら。https://gitlab.com/Jabba14/gatsby_login

ネット上の技術解説記事でレポジトリーが公開されてないと「この記事の通りにやったら本当にこれは動くんかいな」と不安になりますよね。そういう不安ある方はこちらからお試しください。

公式サイトの解説はこちら(このブログ記事では内容をちょっと変えた)

authentication-tutorial

スターターキットから開始する

こちらの Gatsby をはじめる手順にしたがって、まずはスターターキットから選ぶ。

今回は公式スターターキットであるgatsby-starter-blogにした。やっぱり公式の方が手堅い。

このコマンドで入れる。

$ gatsby new gatsby_login https://github.com/gatsbyjs/gatsby-starter-blog

Gatsby ブログラムが走ってインストールされる。

完了したら、フォルダにいって立ち上げる。

$ cd gatsby_login
$ gatsby develop

ブラウザで http://localhost:8000/ にアクセスするとこんなブログができている。

image

確認できたらここまでを git に残しておく。

$ git initial
$ git add .
$ git commit -m ‘initial’

現在のフォルダ構成はこうなっているはず。

▸ content/
▸ node_modules/
▸ public/
▸ src/
▸ static/
  gatsby-browser.js
  gatsby-config.js
  gatsby-node.js
  LICENSE
  package-lock.json
  package.json
  README.md
  yarn.lock

今回、主に変更するのは src/配下のファイルになる。

ナビテーションバーを作る

ナビテーションバーで今のログイン状態やログアウトリンクを表示させるのに使う。そんな機能は components に入れる。

src/ 配下のフォルダ構成はこうなっている。

▾ src/
  ▸ components/
  ▸ pages/
  ▸ templates/
  ▸ utils/

components/の配下に nav-bar.js を作る。

// src/components/nav-bar.js

import React from 'react'
import { Link } from 'gatsby'

export default () => (
  <div
    style={{
      display: 'flex',
      flex: '1',
      justifyContent: 'space-between',
      borderBottom: '1px solid #d1c1e0',
    }}
  >
    <span>You are not logged in</span>

    <nav>
      <Link to="/">Home</Link>
      {` `}
      <Link to="/">Profile</Link>
      {` `}
      <Link to="/">Logout</Link>
    </nav>
  </div>
)

とりあえず Home、Profile、Logout を表示するだけにしておく。

作った nav-bar をレイアウトの中に入れる。既存のファイルに加えたのは import<NavBar /> のみ。

// src/components/layout.js

import React from "react"
import { Link } from "gatsby"

import { rhythm, scale } from "../utils/typography"
import NavBar from "./nav-bar"

const Layout = ({ location, title, children }) => {
  const rootPath = `${__PATH_PREFIX__}/`
  let header

  if (location.pathname === rootPath) {
    header = (
      <>
        <NavBar />
        <h1
          style={{
            ...scale(1.5),
            marginBottom: rhythm(1.5),
            marginTop: 0,
          }}
        >
          <Link
            style={{
              boxShadow: `none`,
              color: `inherit`,
            }}
            to={`/`}
          >
            {title}
          </Link>
        </h1>
      </>
    )
  } else {
    header = (
      <>
        <NavBar />
        <h3
          style={{
            fontFamily: `Montserrat, sans-serif`,
            marginTop: 0,
          }}
        >
          <Link
            style={{
              boxShadow: `none`,
              color: `inherit`,
            }}
            to={`/`}
          >
            {title}
          </Link>
        </h3>
      </>
    )
  }
     // 以下変更無し

この時点で以下のようなナビゲーションバーがヘッダーに表示されているはず。リンクはまだない。

image

Auth サービス

services フォルダを作ってそこに Authentication のロジックを書き込む。

// src/services/auth.js

export const isBrowser = () => typeof window !== 'undefined'

export const getUser = () =>
  isBrowser() && window.localStorage.getItem('gatsbyUser')
    ? JSON.parse(window.localStorage.getItem('gatsbyUser'))
    : {}

const setUser = (user) => window.localStorage.setItem('gatsbyUser', JSON.stringify(user))

export const handleLogin = ({ username, password }) => {
  if (username === `jabba` && password === `password`) {
    return setUser({
      username: `jabba`,
      name: `Jabba`,
      email: `[email protected]`,
    })
  }

  return false
}

export const isLoggedIn = () => {
  const user = getUser()

  return !!user.username
}

export const logout = (callback) => {
  setUser({})
  callback()
}

getUser ユーザー情報の読み取り

setUser ユーザー情報の保存

handleLogin ログイン処理。今回はサーバー側が無いのでログイン名jabbaパスワードpasswordをそのまま書いた。

isLoggedIn 現在のログイン状態の読み取り

logout ログアウト処理。

ログインページの作成

まずは login のコンポーネントから作る。

// src/components/login.js

import React from 'react'
import { navigate } from 'gatsby'
import { handleLogin, isLoggedIn } from '../services/auth'

class Login extends React.Component {
  state = {
    username: ``,
    password: ``,
  }

  handleUpdate = (event) => {
    this.setState({
      [event.target.name]: event.target.value,
    })
  }

  handleSubmit = (event) => {
    event.preventDefault()
    handleLogin(this.state)
  }

  render() {
    return (
      <>
        <h1>Log in</h1>
        <form
          method="post"
          onSubmit={(event) => {
            this.handleSubmit(event)
            navigate(`/`)
          }}
        >
          <label>
            Username
            <input type="text" name="username" onChange={this.handleUpdate} />
          </label>
          <label>
            Password
            <input type="password" name="password" onChange={this.handleUpdate} />
          </label>
          <input type="submit" value="Log In" />
        </form>
      </>
    )
  }
}

export default Login

ログイン用のフォームとしてユーザー名とパスワードを入れるところを表示する。

書き込まれたら onChange で発火させて state を更新する。Submit されたら handleLogin にわたす。

これに合わせて page を作る。

// src/pages/login.js

import React from 'react'
import Layout from '../components/layout'
import Login from '../components/login'

const LoginPage = ({ location }) => (
  <Layout location={location}>
    <Login />
  </Layout>
)

export default LoginPage

page に入れるとそのままパスが作られる。

この時点で http://localhost:8000/login にアクセスが可能になっていて、こんな画面が出る。

image

ユーザー名:jabba パスワード:password

を入れるとログインできる。けど、特になにも起こらない。

プロファイルページを作る

まず profile コンポーネントから。

// src/components/profile.js

import React from 'react'
import { navigate } from 'gatsby'
import { getUser, isLoggedIn } from '../services/auth'

class Profile extends React.Component {
  componentDidMount() {
    if (!isLoggedIn()) {
      navigate(`/login`)
    }
  }

  render() {
    return (
      <>
        <h1>Your profile</h1>
        <ul>
          <li>Name: {getUser().name}</li>
          <li>E-mail: {getUser().email}</li>
        </ul>
      </>
    )
  }
}

export default Profile

やってることは、もしログインしてなかったら login ページへ飛ぶ。ログインしていたら、名前と email を表示する。

// src/pages/profile.js

import React from 'react'
import Layout from '../components/layout'
import Profile from '../components/profile'

const ProfilePage = ({ location }) => (
  <Layout location={location}>
    <Profile />
  </Layout>
)

export default ProfilePage

nav-bar を書き換える。

// src/components/nav-bar.js

import React from "react"
import { Link } from "gatsby"
import { getUser, isLoggedIn, logout } from "../services/auth"

class NavBar extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      greetingMessage: ""
    }
  }

  componentDidMount() {
    let greetingMessage = ""
    if (isLoggedIn()) {
      greetingMessage = `ハロー ${getUser().name} さん`
    } else {
      greetingMessage = "You are not logged in"
    }
    this.setState({
      greetingMessage: greetingMessage
    })
  }

  render() {
    return(
      <div
        style={{
          display: "flex",
          flex: "1",
          justifyContent: "space-between",
          borderBottom: "1px solid #d1c1e0",
        }}
      >
        <span>{this.state.greetingMessage}</span>

        <nav>
          <Link to="/">Home</Link>
          {` `}
          {isLoggedIn()
            ? (
              <>
                <Link to="/profile">Profile</Link>
                {` `}
              </>
            )
            : (
              <Link to=/login”>Login</Link>
            )
          }
        </nav>
      </div>
    )
  }
}

export default NavBar

nav-bar にやったこと。もしログイン中だったら「ハロー ユーザー名」の挨拶を入れる。ログイン中でなかったら”You are not logged in”のメッセージを入れる。メッセージ設定を componentDidMount の中に入れてるのは SSR のエラーを防ぐため。

ここまでで、ログインした状態でトップページを見るとこのように挨拶が出ている。

image

nav-bar のプロファイルリンクも有効になっていて、http://localhost:8000/profile に飛ぶとこのように情報が出ている。

image

logout 処理

nav-bar にログアウト処理を加える。

// src/components/nav-bar.js

import React from 'react'
import { Link } from 'gatsby'
import { getUser, isLoggedIn, logout } from '../services/auth'

class NavBar extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      greetingMessage: '',
    }
  }

  componentDidMount() {
    let greetingMessage = ''
    if (isLoggedIn()) {
      greetingMessage = `ハロー ${getUser().name} さん`
    } else {
      greetingMessage = 'You are not logged in'
    }
    this.setState({
      greetingMessage: greetingMessage,
    })
  }

  logout = (event) => {
    event.preventDefault()
    this.setState({
      greetingMessage: 'You are not logged in',
    })
  }

  render() {
    return (
      <div
        style={{
          display: 'flex',
          flex: '1',
          justifyContent: 'space-between',
          borderBottom: '1px solid #d1c1e0',
        }}
      >
        <span>{this.state.greetingMessage}</span>

        <nav>
          <Link to="/">Home</Link>
          {` `}
          {isLoggedIn() ? (
            <>
              <Link to="/profile">Profile</Link>
              {` `}
              <Link to="/" onClick={logout}>
                Logout
              </Link>
            </>
          ) : (
            <Link to="/login">Login</Link>
          )}
        </nav>
      </div>
    )
  }
}

export default NavBar

やってること。もしログイン状態だったらログアウトリンクを表示して、クリックされたらログアウト処理してトップページへ飛ぶ。React ではそのコンポーネントの state が更新されると再評画が走る。logout メソッドの中で greetingMessage を更新しているので、再評画されて新しいメッセージが出る。

ここまでで Logout リンクが出て押すとログアウトされて”You are not logged in“の表示が出る。

image

コンテンツのリンクを整える

Gatsby の content フォルダにマークダウンで入れたファイルは自動的にページとして生成される。その際のリンクもフォルダ名が入る。例えば今の状態だとひとつめのブログ記事「New Beginnings」はhttp://localhost:8000/new-beginnings/に入っている。このままではいつか login とか profile なんて記事をもし書いたら衝突が起きてしまう。

ブログの場合はそこを絶対に衝突させないようによく日付を数列にして入れている。

例えばhello-worldというフォルダを20200401-hello-worldという風に。これだと同じ日に同じタイトルを入れるようなことをしない限りは衝突が起きない。

後はこちらの手順にしたがって、Netlify なり GitLab pages にデプロイしてできあがり。

ソースコードはこちらに置いたので細かいところはこちらを参照してください。少なくともこのコードは動いてる。

https://gitlab.com/Jabba14/gatsby_login

最終的にできあがったウェブサイトがこんな感じになった。

https://jabba14.gitlab.io/gatsby_login/

image

まとめ

静的サイトジェネレーターといっても基本は React ベースなのでなんでもできる。Gatsby の爆速機能にのっかって、そこに自分が欲しい分だけを追加するのもいい方法。

今回はサーバー側については一切書いていない。なぜなら Gatsby + Firebase とかの連携に興味ある人が居るのか、ちょっと疑問だったから。まずは「Gatsby は静的とは言ってもフルカスタマイズ可能だし、いろいろできますよ」とだけ言いたかったのでこの記事を書いた。

関連記事