概要
前回まででHugoとGraphCMSを連携させてGithu Pagesで公開する方法を解説した。
今回、番外編として国際化対応の実装を行う。
国際化対応
ウェブ制作の案件として国際化対応についてはどれほど需要があるかわからないが、HugoもGraphCMSもi18nを簡単に導入できるので説明に加えることにした。
GraphCMSの設定
GraphCMSはフリープランでは二カ国語まで対応している。
デフォルトがen
なのでまず日本語のja
を加える。
ロケールの追加設定
ダッシュボード>Settings
>Locales
を開く。
Dispyal name
にJapanese
を選びAdd
をクリック。
Japanese
とJapanese(Japanese)
と二つあってややこしいが、クエリー結果の出力名称(API名)の違いである。
-
Japanese
:ja
-
Japanese(Japanese)
:ja_JP
なのでその後のスクリプト内での表記法が異なるだけなので別にどちらをでもよい。
ただし今回はこの名称をHugoの言語別のディレクトリ名に使うのでen
, ja
となるほうを採用した。
スキーマ定義修正
設定にロケールを追加しただけでは何も変わらない。
モデルのフィールド毎にロケールの設定を行う必要がある。
今回作成したpost
モデルの場合は多言語化対象になるのはtitle
, body
, tag
が対象となる。
slugやdateなどは言語が関係ないので修正不要である。
まずtitle
を修正する。
title
のEdit field
をクリックしLocalized field
にチェックをいれる。
同様にbody
やtag
も設定する。
追加設定
ここで追加の設定を行う。
ブログを二ヶ国語化した場合でも必ずしも二ヶ国語で投稿したいとは限らない。
国際的な話題は二ヶ国語化したいが、基本は日本語だけで十分だと言う場合もあるし。
逆に英語でお知らせしたいが日本語は不要の場合もあると考えられる。
そのためモデルのスキーマ定義に言語設定用のフィールドを追加して出力する言語を選べるようにする。
スキーマ定義の左のEnumerations
>Add
をクリック。
Name
をlocale_list
としてja
とen
を加えた。
そしてpost
モデルのフィールドにDropdown
として追加する。
最終的なスキーマ定義はこんな感じ。
invalid_locale
は選択された言語をビルドの対象から外すオプションだ。
必須項目とはせず、ja
を選択したら日本語、en
を選択したら英語を対象外とする。
デフォルトでは何も選択しないのでその場合は両方出力となるる。
コンテンツ修正
このままではまだ国際化対応はできていない。
そこで既存のコンテンツを修正する。新規に作る場合も要領は同じ。
右側にLOCALIZATIONS
と表示されているところがありEnglish
のみがチェック入っている。
Japanese
も同様にチェックすると、Localized
対象のフィールドのtitle
などが該当言語ごとに追加される。
既存のコンテンツはこのままではデフォルトのen
に日本語が入っているので面倒だが、ja
のところに移す。
invalid_locale
でen
を選択すると英語は不要になるのだけども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
のメニューが追加されていることを確認していただきたい。
そしてこのen
をクリックするとページは英語表記に変わり言語メニューはja
となる。
ブログ詳細ページも同様に言語メニューが出るのだが、先程日本語のみの出力設定(invalid_locale
にen
を設定)を行ったコンテンツはこんなふうになり、日本語でしか表示されない。
そもそも先に紹介したトップページの日本語画面、英語画面のところに表示されるブログの一覧のところで既に違いが出ている。
完成したソース
上記で作成したサイト
まとめ
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で同様のことが実現できると思われるのでもっとこういった活用事例が増えるとみんなハッピーになれるかなと思う。