LoginSignup
0

More than 1 year has passed since last update.

posted at

updated at

GraphCMSとHugoを連携してGithub Pagesで公開する(番外編)〜国際化対応

概要

前回まででHugoとGraphCMSを連携させてGithu Pagesで公開する方法を解説した。
今回、番外編として国際化対応の実装を行う。

国際化対応

ウェブ制作の案件として国際化対応についてはどれほど需要があるかわからないが、HugoもGraphCMSもi18nを簡単に導入できるので説明に加えることにした。

GraphCMSの設定

GraphCMSはフリープランでは二カ国語まで対応している。
デフォルトがenなのでまず日本語のjaを加える。

ロケールの追加設定

ダッシュボード>Settings>Localesを開く。
Dispyal nameJapaneseを選びAddをクリック。

FireShot Capture 139 - GraphCMS - app.graphcms.com1.png

FireShot Capture 139 - GraphCMS - app.graphcms.com.png

JapaneseJapanese(Japanese)と二つあってややこしいが、クエリー結果の出力名称(API名)の違いである。

  • Japanese: ja
  • Japanese(Japanese): ja_JP

なのでその後のスクリプト内での表記法が異なるだけなので別にどちらをでもよい。
ただし今回はこの名称をHugoの言語別のディレクトリ名に使うのでen, jaとなるほうを採用した。

スキーマ定義修正

設定にロケールを追加しただけでは何も変わらない。
モデルのフィールド毎にロケールの設定を行う必要がある。
今回作成したpostモデルの場合は多言語化対象になるのはtitle, body, tagが対象となる。

slugやdateなどは言語が関係ないので修正不要である。

まずtitleを修正する。
titleEdit fieldをクリックしLocalized fieldにチェックをいれる。

FireShot Capture 136 - GraphCMS - app.graphcms.com.png

同様にbodytagも設定する。

追加設定

ここで追加の設定を行う。
ブログを二ヶ国語化した場合でも必ずしも二ヶ国語で投稿したいとは限らない。
国際的な話題は二ヶ国語化したいが、基本は日本語だけで十分だと言う場合もあるし。
逆に英語でお知らせしたいが日本語は不要の場合もあると考えられる。
そのためモデルのスキーマ定義に言語設定用のフィールドを追加して出力する言語を選べるようにする。

スキーマ定義の左のEnumerations>Addをクリック。
Namelocale_listとしてjaenを加えた。

そしてpostモデルのフィールドにDropdownとして追加する。

FireShot Capture 151 - GraphCMS - app.graphcms.com.png

最終的なスキーマ定義はこんな感じ。

FireShot Capture 152 - GraphCMS - app.graphcms.com.png

invalid_locale選択された言語をビルドの対象から外すオプションだ。
必須項目とはせず、jaを選択したら日本語、enを選択したら英語を対象外とする
デフォルトでは何も選択しないのでその場合は両方出力となるる。

コンテンツ修正

このままではまだ国際化対応はできていない。
そこで既存のコンテンツを修正する。新規に作る場合も要領は同じ。

右側にLOCALIZATIONSと表示されているところがありEnglishのみがチェック入っている。
Japaneseも同様にチェックすると、Localized対象のフィールドのtitleなどが該当言語ごとに追加される。

FireShot Capture 145 - GraphCMS - app.graphcms.com.png

既存のコンテンツはこのままではデフォルトのenに日本語が入っているので面倒だが、jaのところに移す。
invalid_localeenを選択すると英語は不要になるのだけどもtitleなどのフィールド自体は必須項目なので何か入れる必要がある。

確認

ダッシュボードのAPI Playgroundから以下のクエリーをなげて確認する。

query MyQuery {
  posts(locales: [ja, en], orderBy: updatedAt_DESC) {
    localizations(includeCurrent: true) {
      id
      title
      slug
      date
      eyecatch {
        url
      }
      body
      tag
      locale
      invalidLocale
      updatedAt
    }
  }
}

Hugoの設定

続いてHugoの設定。

config.tomlに言語設定を追加する

# 省略

[languages]
  [languages.en]
    title = "My blog"
    weight = 2
    contentDir = "content/en"
  [languages.ja]
    title = "私のブログ"
    weight = 1
    contentDir = "content/ja"

# 省略

上記でわかる通り出力ディレクトリが変わるので以下のような構成にする。

content
├── en
│   ├── _index.md
│   └── post
│       └── _index.md
└── ja
    ├── _index.md
    └── post
        └── _index.md

Pythonの設定

受け入れ体制が整ったのでスクリプトの修正を行う。
細かい解説はしないので完成形をお見せする。

app/__main__.py

import datetime
import json
import os
import pathlib
import re
import urllib.request

from dotenv import load_dotenv

APP_DIR = os.path.abspath(os.path.dirname(__file__))
PROJECT_DIR = pathlib.Path(APP_DIR).parent
HUGO_CONTENT_DIR = os.path.join(PROJECT_DIR, 'content')
HUGO_POST_DIRNAME = 'post'
UPDATE_SEC = int(os.getenv('UPDATE_SEC', '300'))

dotenv_path = os.path.join(PROJECT_DIR, '.env')
load_dotenv(dotenv_path)


class GraphcmsManager(object):
    def __init__(self, endpoint, token):
        self.endpoint = endpoint
        self.headers = {'Authorization': f'Bearer {token}'}

    def __format_query(self, s):
        s = re.sub(r'\s+', '' ' ', s).replace('\n', ' ')
        return {'query': s}

    def __query_statement(self):
        return '''\
        {
          posts(locales: [ja, en], orderBy: updatedAt_DESC) {
            localizations(includeCurrent: true) {
              id
              title
              slug
              date
              eyecatch {
                url
              }
              body
              tag
              locale
              validLocale
              updatedAt
            }
          }
        }'''

    def query(self, data=None, is_raw=True):
        if not data:
            data = self.__query_statement()
        if is_raw:
            data = self.__format_query(data)

        req = urllib.request.Request(self.endpoint,
                                     data=json.dumps(data).encode(),
                                     headers=self.headers)
        status_code = 500
        try:
            with urllib.request.urlopen(req) as response:
                payload = json.loads(response.read())
                status_code = response.getcode()
        except urllib.error.HTTPError as e:
            payload = {'error': e.reason}
        except urllib.error.URLError as e:
            payload = {'error': e.reason}
        except Exception as e:
            payload = {'error': str(e)}
        return status_code, payload

    def __time_diff(self, date_str, fmt='%Y-%m-%dT%H:%M:%S.%f+00:00'):
        updatetime = datetime.datetime.strptime(date_str, fmt)
        return (datetime.datetime.now() - datetime.timedelta(hours=9) - updatetime).seconds

    def gen_hugo_contents(self, payload):
        result = list()

        data = (payload.get('data'))
        for model, content_list in data.items():
            for content in content_list:
                for x in content.get('localizations'):
                    data_map = dict()
                    locale = x['locale']
                    if locale == x.get('validLocale'):
                        print(f'Pass language code {locale} for content {x["id"]}')
                        continue
                    front_matter = f'title: "{x["title"]}"\n'
                    front_matter += f'slug: "{x["slug"]}"\n'
                    front_matter += f'date: {x["date"]}\n'
                    eyecatch = x.get('eyecatch')
                    if eyecatch:
                        front_matter += f'featured_image: {eyecatch["url"]}\n'
                    tag = x.get('tag')
                    if tag:
                        front_matter += f'tags: {str(tag)}\n'

                    data_map['front_matter'] = front_matter
                    data_map['body'] = x['body']
                    data_map['filepath'] = f'{x["id"]}.md'
                    data_map['update_sec'] = self.__time_diff(x['updatedAt'])
                    data_map['locale'] = locale

                    result.append(data_map)

        return result

    def write(self, data, update_sec=UPDATE_SEC):
        for x in data:
            fullpath = os.path.join(HUGO_CONTENT_DIR, x['locale'],
                                    HUGO_POST_DIRNAME,  x['filepath'])
            os.makedirs(os.path.dirname(fullpath), exist_ok=True)

            if os.path.exists(fullpath) and x["update_sec"] > update_sec:
                # skip old posts
                continue

            with open(fullpath, 'w') as f:
                text = f'---\n{x["front_matter"]}---\n{x["body"]}'
                f.write(text)


def main():
    endpoint = os.getenv('GRAPHCMS_ENDPOINT', 'http://localhost')
    token = os.getenv('GRAPHCMS_TOKEN', 'my-token')
    G = GraphcmsManager(endpoint=endpoint, token=token)
    status_code, payload = G.query()
    if status_code != 200:
        print(payload)
        return
    data = G.gen_hugo_contents(payload)
    G.write(data)


if __name__ == "__main__":
    main()

コンテンツ取得、ビルドおよび開発サーバ起動

python -m app
hugo serve

結果

まずトップページはこんな感じになる。
右上にenのメニューが追加されていることを確認していただきたい。

FireShot Capture 146 - 投稿記事 - 私のブログ - localhost.png

そしてこのenをクリックするとページは英語表記に変わり言語メニューはjaとなる。

FireShot Capture 147 - Blog posts - My blog - localhost.png

ブログ詳細ページも同様に言語メニューが出るのだが、先程日本語のみの出力設定(invalid_localeenを設定)を行ったコンテンツはこんなふうになり、日本語でしか表示されない。

FireShot Capture 153 - オリーブ収穫 - 私のブログ - localhost.png

そもそも先に紹介したトップページの日本語画面、英語画面のところに表示されるブログの一覧のところで既に違いが出ている。

完成したソース

上記で作成したサイト

まとめ

HugoとGraphCMSとの連携の番外編として国際化の説明を行った。

実際の業務案件としてウェブサイトをリニューアルして二ヶ国語対応したいというのがあり、Hugoでやってみたいと思ったのがきっかけだった。
Hugoでやる場合、ネックになるのがMarkdown記法である。私は慣れているが一般的な利用者向きとはいえず、テキストエディタで投稿するのは現実的ではなかった。

そこで目をつけたのがヘッドレスCMS。
最初はContentfulを使ってみた。以前紹介したcontentful-hugoというライブラリを利用すれば連携できることはわかった。
しかし、Node.jsで書かれたcontentful-hugoのカスタマイズに苦労したり、またCotentful側のAPIのi18n対応が思った結果を取得できなかったことからいろいろ模索してGraphCMSにたどり着いた。さらにPythonでスクリプトを書くことで自分の土俵にもってくることができた。

  • ヘッドレスCMSのWEB-APIを利用してコンテンツを取得する
  • 上記をMarkdown形式で書き出す

というたった二つのことを具象化するだけでHugoとヘッドレスCMSの相乗効果を引き出せるのはすごいことだと思う。

今回紹介したGraphCMS(と以前紹介したContentful)に限らずいろんなヘッドレスCMSで同様のことが実現できると思われるのでもっとこういった活用事例が増えるとみんなハッピーになれるかなと思う。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0