AWS Lambdaを実装するときは最初にeventを確認すべき

前回までのまとめ

前回、bqコマンドを使ってデータセットを管理できることを確認しました。今回はcloud functionsを使ってエンドポイントを実装する予定でしたが、いろいろあってAWSを利用することになりました。その中でKinesis Data FirehoseからS3にデータをPUTする前にAWS Lambdaでデータを整形する必要が出てきました。その時に気づいた(当たり前かもしれない)ことを記載します。

関数の作成

まずはじめに関数を作成します。

AWSコンソール画面からAWS Lambdaを選択して、関数の作成をクリックします。

設計図の使用を選び、設計図の検索フォームにはFirehoseと入力して検索します。すると、kinesis-firehose-process-recordという設計図が出てくるので、こちらを選択します。

関数名に適切な名前を入力し、画面右下にある関数の作成ボタンをクリックします。これでLambda関数が作成できました。

コードの確認

設計図から作成されたコードは以下のようになっていました。

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('Loading function');

exports.handler = async (event, context) => {
/* Process the list of records and transform them */
const output = event.records.map((record) => ({
/* This transformation is the "identity" transformation, the data is left intact */
recordId: record.recordId,
result: 'Ok',
data: record.data,
}));
console.log(`Processing completed. Successful records ${output.length}.`);
return { records: output };
};

event.recordsでストリームデータにアクセスできるようです。map内のコールバック関数で各データにアクセスができます。今回はrecord.dataを整形して返せばよさそうです。

データ構造がわからずハマる

実装を始めようと思い、record.dataをいろいろいじって見たのですが、どうにもうまくいきません。そもそもどういうデータなのかわかっていなかったのがよくなかったです…

いろいろ調べていると以下のページが見つかりました。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/with-kinesis-example.html

このサンプルに

1
//console.log(JSON.stringify(event, null, 2));

と書かれていて、嗚呼、最初にこれを実行すればよかったんだと気づきました。

eventを確認して実装する

eventは以下のようになっていました

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"invocationId": "354ca9e9-7e83-4b8a-8415-7efddfd276a1",
"deliveryStreamArn": "arn:aws:firehose:ap-northeast-1:000000000:deliverystream/sample-stream",
"region": "ap-northeast-1",
"records": [
{
"recordId": "49612424225054340516134952530760216311574238310848004098000000",
"approximateArrivalTimestamp": 1605063330351,
"data": "eyJjb250YWluZXJfaWQiOiJhYmYzYjJkZGQyNWJlYWQzMjIwZjU1MTliYTM0MzZlZjhmNDNhN2QwYzg1MTdjZDhmNGViN2M2NDIyZjY4NzVmIiwiY29udGFpbmVyX25hbWUiOiIvZWNzLWFkZmFpci1hbmFseXRpY3MtZmFyZ2F0ZS10YXNrLTEzLWFkZmFpci1hbmFseXRpY3MtYXAtZGNiMWRiOWY4OWEyZTBlOGQzMDEiLCJlY3NfY2x1c3RlciI6ImFybjphd3M6ZWNzOmFwLW5vcnRoZWFzdC0xOjQ5MzkwNTA5Njc0NTpjbHVzdGVyL2FkZmFpci1hbmFseXRpY3MtZmFyZ2F0ZS1jbHVzdGVyIiwiZWNzX3Rhc2tfYXJuIjoiYXJuOmF3czplY3M6YXAtbm9ydGhlYXN0LTE6NDkzOTA1MDk2NzQ1OnRhc2svYWRmYWlyLWFuYWx5dGljcy1mYXJnYXRlLWNsdXN0ZXIvZDVjM2U3Yzg0ODNlNDQwNDk4ZjhjYjVhZTQ2MmUyNTYiLCJlY3NfdGFza19kZWZpbml0aW9uIjoiYWRmYWlyLWFuYWx5dGljcy1mYXJnYXRlLXRhc2s6MTMiLCJsb2ciOiJ7XCJhXCI6XCJiXCIsXCJjXCI6XCJkXCJ9Iiwic291cmNlIjoic3Rkb3V0In0K"
},
{
"recordId": "49612424225054340516134952530761425237393852940022710274000000",
"approximateArrivalTimestamp": 1605063330354,
"data": "eyJjb250YWluZXJfaWQiOiJhYmYzYjJkZGQyNWJlYWQzMjIwZjU1MTliYTM0MzZlZjhmNDNhN2QwYzg1MTdjZDhmNGViN2M2NDIyZjY4NzVmIiwiY29udGFpbmVyX25hbWUiOiIvZWNzLWFkZmFpci1hbmFseXRpY3MtZmFyZ2F0ZS10YXNrLTEzLWFkZmFpci1hbmFseXRpY3MtYXAtZGNiMWRiOWY4OWEyZTBlOGQzMDEiLCJlY3NfY2x1c3RlciI6ImFybjphd3M6ZWNzOmFwLW5vcnRoZWFzdC0xOjQ5MzkwNTA5Njc0NTpjbHVzdGVyL2FkZmFpci1hbmFseXRpY3MtZmFyZ2F0ZS1jbHVzdGVyIiwiZWNzX3Rhc2tfYXJuIjoiYXJuOmF3czplY3M6YXAtbm9ydGhlYXN0LTE6NDkzOTA1MDk2NzQ1OnRhc2svYWRmYWlyLWFuYWx5dGljcy1mYXJnYXRlLWNsdXN0ZXIvZDVjM2U3Yzg0ODNlNDQwNDk4ZjhjYjVhZTQ2MmUyNTYiLCJlY3NfdGFza19kZWZpbml0aW9uIjoiYWRmYWlyLWFuYWx5dGljcy1mYXJnYXRlLXRhc2s6MTMiLCJsb2ciOiJ7XCJhXCI6XCJiXCIsXCJjXCI6XCJkXCJ9Iiwic291cmNlIjoic3Rkb3V0In0K"
},
{
"recordId": "49612424225054340516134952530762634163213467569197416450000000",
"approximateArrivalTimestamp": 1605063330354,
"data": "eyJjb250YWluZXJfaWQiOiJhYmYzYjJkZGQyNWJlYWQzMjIwZjU1MTliYTM0MzZlZjhmNDNhN2QwYzg1MTdjZDhmNGViN2M2NDIyZjY4NzVmIiwiY29udGFpbmVyX25hbWUiOiIvZWNzLWFkZmFpci1hbmFseXRpY3MtZmFyZ2F0ZS10YXNrLTEzLWFkZmFpci1hbmFseXRpY3MtYXAtZGNiMWRiOWY4OWEyZTBlOGQzMDEiLCJlY3NfY2x1c3RlciI6ImFybjphd3M6ZWNzOmFwLW5vcnRoZWFzdC0xOjQ5MzkwNTA5Njc0NTpjbHVzdGVyL2FkZmFpci1hbmFseXRpY3MtZmFyZ2F0ZS1jbHVzdGVyIiwiZWNzX3Rhc2tfYXJuIjoiYXJuOmF3czplY3M6YXAtbm9ydGhlYXN0LTE6NDkzOTA1MDk2NzQ1OnRhc2svYWRmYWlyLWFuYWx5dGljcy1mYXJnYXRlLWNsdXN0ZXIvZDVjM2U3Yzg0ODNlNDQwNDk4ZjhjYjVhZTQ2MmUyNTYiLCJlY3NfdGFza19kZWZpbml0aW9uIjoiYWRmYWlyLWFuYWx5dGljcy1mYXJnYXRlLXRhc2s6MTMiLCJsb2ciOiJ7XCJhXCI6XCJiXCIsXCJjXCI6XCJkXCJ9Iiwic291cmNlIjoic3Rkb3V0In0K"
}
]
}

record.dataはbase64エンコードされたデータということがわかりました。これでやっと実装ができそうです。

まとめ

Lambdaを実装するときはまずeventの中身を確認するようにしましょう。

次回はAWS Lambdaのデプロイについて記載しようと思います。

bqコマンドでデータセットを管理する

前回までのまとめ

前回、bqコマンドが使えるようセットアップを行いました。今回はbqコマンドを使ってデータセット・スキーマを作成していきたいと思います。

データセットとは

データセットとはなんでしょうか。こちらに書かれていました。RDBでいうデータベースのようなものと思っておけばよさそうです。

データセットの操作

データセットの作成

こちらにあるように、bq mkコマンドを利用します。

1
2
$ bq mk test
Dataset 'project_id:test' successfully created.

無事作成できました。Webコンソールでも確認できます。

データセットの削除

削除にはbq rmコマンドを利用します。

1
2
$ bq rm test
rm: remove dataset 'project_id:test'? (y/N) y

yを入力することで削除できます。確認しなくてもよい場合は-fオプションを指定します。

1
2
$ bq rm -f test
$

メッセージは特に表示されませんが削除されています。

テーブルが存在する場合は-rオプションを指定することで、テーブルごと削除できます。
テーブルを作成するため、スキーマを記載したJSONファイルを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cat t1.json
[
{
"name": "id",
"type": "INT64",
"mode": "REQUIRED"
},
{
"name": "name",
"type": "STRING",
"mode": "REQUIRED"
}
]

JSONファイルからテーブルを作成してみます。

1
2
3
4
5
$ bq mk --table project_id:test.t1 ./t1.json
Table 'project_id:test.t1' successfully created.
$
$ bq show --schema test.t1
[{"name":"id","type":"INTEGER","mode":"REQUIRED"},{"name":"name","type":"STRING","mode":"REQUIRED"}]

無事作成できました。

では、テーブルが存在するデータセットを削除してみます。

1
2
3
$ bq rm test
rm: remove dataset 'project_id:test'? (y/N) y
BigQuery error in rm operation: Dataset project_id:test is still in use

テーブルがあると削除できないようです。-rオプションを指定してみます。

1
2
3
$ bq rm -r test
rm: remove dataset 'project_id:test'? (y/N) y
$

削除できました。本来であれば、テーブルを削除後、データセットを削除すべきです。テーブルの削除はテーブル名を指定して行います。

1
2
$ bq rm test.t1
rm: remove table 'project_id:test.t1'? (y/N) y

テーブルを削除すればデータセットも削除できます。

1
2
3
$ bq rm test
rm: remove dataset 'project_id:test'? (y/N) y
$

テーブルの操作

データセットのところで、テーブルについてもいくつか記載しました。ここでまとめます。

JSONファイルによるテーブルの作成

テーブルの作成を行うにはJSONファイルでスキーマを定義してbqコマンドで作成するのが一番楽そうです。

1
2
3
4
$ bq mk test
Dataset 'project_id:test' successfully created.
$ bq mk --table test.t1 t1.json
Table 'project_id:test.t1' successfully created.

スキーマの確認

bq showでスキーマなど確認できます。

1
2
3
4
5
6
7
$ bq show test.t1
Table project_id:test.t1

Last modified Schema Total Rows Total Bytes Expiration Time Partitioning Clustered Fields Labels
----------------- ---------------------------- ------------ ------------- ------------ ------------------- ------------------ --------
28 Oct 21:49:31 |- id: integer (required) 0 0
|- name: string (required)

スキーマのみ確認したい場合は--schemaオプションを指定します。

1
2
$ bq show --schema test.t1
[{"name":"id","type":"INTEGER","mode":"REQUIRED"},{"name":"name","type":"STRING","mode":"REQUIRED"}]

既存のスキーマから新しいスキーマを作るときに参考にできそうですね。

まとめ

前回インストールしたbqコマンドでデータセット、テーブルの基本的さ操作を行なってみました。次はCloud Functionsに挑戦してみようと思います。

bqコマンドをインストールする

背景

仕事でBigQueryを利用することになりました。今までのBigQueryとの接点は、GoogleAnalyticsのデータに対してクエリを実行するくらいでした。
色々わかっていないところがあるので、これを機にしっかりと理解しようと思い、オライリーのGoogle BigQueryを読もうと思いました(5年前の本ですが、全体をざっとみて知りたい内容が多かったので購入しました)

読み進めていると、bqコマンドを利用する箇所がいくつか出てきました。最初は無視して読み進めていたのですが、実際に手を動かしてデータを扱う箇所でもbqコマンドを利用していました。これは環境を準備するしかないと思い、bqコマンドのインストールを行いました。その時のメモです。

全体の流れ

インストールの全体の流れはちゃんとドキュメントが用意されています。

bqコマンドラインツールの利用

このページにある

Cloud SDKをインストールして初期化しますのところをやっていきます。

Google Cloud SDK のインストール

先ほどのリンクにある手順通り行っていきます。

Pythonのインストール

まずはPythonをインストールします。最新のバージョンをインストールすればよいかと思いきや、サポートされているバージョンは 3.5~3.7、2.7.9 以降です。ということなので、3.7の最新版をインストールしようと思います。

まずは、pyenvを最新にします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ pyenv
pyenv 1.2.19-6-gbdfed51d
(省略)
$ anyenv update pyenv
Updating 'pyenv'...
| From https://github.com/yyuu/pyenv
| bdfed51d..806b30d6 master -> origin/master
| * [new tag] v1.2.21 -> v1.2.21
| * [new tag] v1.2.20 -> v1.2.20
Skipping 'pyenv/python-build'; not git repo
Updating 'anyenv manifest directory'...
| From https://github.com/anyenv/anyenv-install
| dcbcfe1..d9791df master -> origin/master
$ pyenv
pyenv 1.2.21
(省略)

次に、インストールするバージョンを確認します。

1
2
3
4
5
6
$ pyenv install -l
(省略)
3.7.8
3.7.9
3.8.0
(省略)

3.7系の最新は3.7.9なので3.7.9をインストールします。

1
2
3
4
5
6
7
8
9
10
11
12
$ pyenv install 3.7.9
Downloading openssl-1.1.0j.tar.gz...
-> https://www.openssl.org/source/old/1.1.0/openssl-1.1.0j.tar.gz
Installing openssl-1.1.0j...
Installed openssl-1.1.0j to /Users/user/.anyenv/envs/pyenv/versions/3.7.9

python-build: use readline from homebrew
Downloading Python-3.7.9.tar.xz...
-> https://www.python.org/ftp/python/3.7.9/Python-3.7.9.tar.xz
Installing Python-3.7.9...
python-build: use readline from homebrew
Installed Python-3.7.9 to /Users/user/.anyenv/envs/pyenv/versions/3.7.9

3.7.9を利用するように設定します。

1
2
3
$ pyenv local 3.7.9
$ python --version
Python 3.7.9

Pythonのインストールは完了しました。

SDKのダウンロード

SDKをダウンロードします。
64ビット版をダウンロードします。

アーカイブをファイル システム上の任意の場所に展開します。ホーム ディレクトリを使用することをおすすめします。とのことなので、ホームディレクトリ下に展開します。

1
2
3
4
5
6
7
8
9
$ tar xzf ~/Downloads/google-cloud-sdk-312.0.0-darwin-x86_64.tar.gz -C .
$ cd google-cloud-sdk
$ ls
LICENSE completion.zsh.inc path.bash.inc
README data path.fish.inc
RELEASE_NOTES deb path.zsh.inc
VERSION install.bat platform
bin install.sh properties
completion.bash.inc lib rpm

インストールスクリプトの実行

展開したファイルの中のinstall.shを実行します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
$ cd ..
$ ./google-cloud-sdk/install.sh
Welcome to the Google Cloud SDK!

To help improve the quality of this product, we collect anonymized usage data
and anonymized stacktraces when crashes are encountered; additional information
is available at <https://cloud.google.com/sdk/usage-statistics>. This data is
handled in accordance with our privacy policy
<https://policies.google.com/privacy>. You may choose to opt in this
collection now (by choosing 'Y' at the below prompt), or at any time in the
future by running the following command:

gcloud config set disable_usage_reporting false

Do you want to help improve the Google Cloud SDK (y/N)? y


Your current Cloud SDK version is: 312.0.0
The latest available version is: 315.0.0

┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Components │
├──────────────────┬──────────────────────────────────────────────────────┬──────────────────────────┬──────────┤
│ Status │ Name │ ID │ Size │
├──────────────────┼──────────────────────────────────────────────────────┼──────────────────────────┼──────────┤
│ Update Available │ BigQuery Command Line Tool │ bq │ < 1 MiB │
│ Update Available │ Cloud SDK Core Libraries │ core │ 15.4 MiB │
│ Update Available │ Cloud Storage Command Line Tool │ gsutil │ 3.5 MiB │
│ Not Installed │ App Engine Go Extensions │ app-engine-go │ 4.8 MiB │
│ Not Installed │ Appctl │ appctl │ 18.5 MiB │
│ Not Installed │ Cloud Bigtable Command Line Tool │ cbt │ 7.6 MiB │
│ Not Installed │ Cloud Bigtable Emulator │ bigtable │ 6.6 MiB │
│ Not Installed │ Cloud Datalab Command Line Tool │ datalab │ < 1 MiB │
│ Not Installed │ Cloud Datastore Emulator │ cloud-datastore-emulator │ 18.4 MiB │
│ Not Installed │ Cloud Firestore Emulator │ cloud-firestore-emulator │ 42.1 MiB │
│ Not Installed │ Cloud Pub/Sub Emulator │ pubsub-emulator │ 56.3 MiB │
│ Not Installed │ Cloud SQL Proxy │ cloud_sql_proxy │ 7.3 MiB │
│ Not Installed │ Emulator Reverse Proxy │ emulator-reverse-proxy │ 14.5 MiB │
│ Not Installed │ Google Cloud Build Local Builder │ cloud-build-local │ 6.2 MiB │
│ Not Installed │ Google Container Registry's Docker credential helper │ docker-credential-gcr │ 1.8 MiB │
│ Not Installed │ Kind │ kind │ 4.4 MiB │
│ Not Installed │ Kustomize │ kustomize │ 22.8 MiB │
│ Not Installed │ Minikube │ minikube │ 24.0 MiB │
│ Not Installed │ Nomos CLI │ nomos │ 17.6 MiB │
│ Not Installed │ Skaffold │ skaffold │ 15.3 MiB │
│ Not Installed │ anthos-auth │ anthos-auth │ 16.2 MiB │
│ Not Installed │ gcloud Alpha Commands │ alpha │ < 1 MiB │
│ Not Installed │ gcloud Beta Commands │ beta │ < 1 MiB │
│ Not Installed │ gcloud app Java Extensions │ app-engine-java │ 59.5 MiB │
│ Not Installed │ gcloud app PHP Extensions │ app-engine-php │ 21.9 MiB │
│ Not Installed │ gcloud app Python Extensions │ app-engine-python │ 6.1 MiB │
│ Not Installed │ gcloud app Python Extensions (Extra Libraries) │ app-engine-python-extras │ 27.1 MiB │
│ Not Installed │ kpt │ kpt │ 11.7 MiB │
│ Not Installed │ kubectl │ kubectl │ < 1 MiB │
│ Not Installed │ pkg │ pkg │ │
└──────────────────┴──────────────────────────────────────────────────────┴──────────────────────────┴──────────┘
To install or remove components at your current SDK version [312.0.0], run:
$ gcloud components install COMPONENT_ID
$ gcloud components remove COMPONENT_ID

To update your SDK installation to the latest version [315.0.0], run:
$ gcloud components update


Modify profile to update your $PATH and enable shell command
completion?

Do you want to continue (Y/n)? Y

The Google Cloud SDK installer will now prompt you to update an rc
file to bring the Google Cloud CLIs into your environment.

Enter a path to an rc file to update, or leave blank to use
[/Users/user/.bash_profile]:
Backing up [/Users/user/.bash_profile] to [/Users/user/.bash_profile.backup].
[/Users/user/.bash_profile] has been updated.

==> Start a new shell for the changes to take effect.


For more information on how to get started, please visit:
https://cloud.google.com/sdk/docs/quickstarts

最新版のSDKに更新

インストールスクリプト実行中の出力に

1
2
To update your SDK installation to the latest version [315.0.0], run:
$ gcloud components update

とあったので、最新版のSDKに更新します。

.bash_profileに書かれた環境変数を読み込むためにシェルを再起動します。

1
$ exec $SHELL -l

上記に書いてあるように最新のSDKに更新します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ gcloud components update


Your current Cloud SDK version is: 312.0.0
You will be upgraded to version: 315.0.0

┌─────────────────────────────────────────────────────────┐
│ These components will be updated. │
├─────────────────────────────────┬────────────┬──────────┤
│ Name │ Version │ Size │
├─────────────────────────────────┼────────────┼──────────┤
│ BigQuery Command Line Tool │ 2.0.62 │ < 1 MiB │
│ Cloud SDK Core Libraries │ 2020.10.16 │ 15.4 MiB │
│ Cloud Storage Command Line Tool │ 4.53 │ 3.5 MiB │
│ gcloud cli dependencies │ 2020.10.02 │ 10.6 MiB │
└─────────────────────────────────┴────────────┴──────────┘

The following release notes are new in this upgrade.

(省略)

Update done!

To revert your SDK to the previously installed version, you may run:
$ gcloud components update --version 312.0.0

これで更新が完了しました。

gcloud initの実行

gcloud initを実行してSDKを初期貸します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ gcloud init
Welcome! This command will take you through the configuration of gcloud.

Your current configuration has been set to: [default]

You can skip diagnostics next time by using the following flag:
gcloud init --skip-diagnostics

Network diagnostic detects and fixes local network connection issues.
Checking network connection...done.
Reachability Check passed.
Network diagnostic passed (1/1 checks passed).

You must log in to continue. Would you like to log in (Y/n)? Y

ここで、Yを入力すると、アカウント確認画面が表示されます。アカウントを選択すると、ブラウザにOAuthの認可画面が表示されます。許可をクリックします。すると、 Google Cloud SDK 認証の完了と表示されます。

ターミナルに戻るとプロジェクトを選択するようになっているので、これから利用するプロジェクトを選択します。数字を入力しエンターを押すと以下のようなメッセージが表示されます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Not setting default zone/region (this feature makes it easier to use
[gcloud compute] by setting an appropriate default value for the
--zone and --region flag).
See https://cloud.google.com/compute/docs/gcloud-compute section on how to set
default compute region and zone manually. If you would like [gcloud init] to be
able to do this for you the next time you run it, make sure the
Compute Engine API is enabled for your project on the
https://console.developers.google.com/apis page.

Created a default .boto configuration file at [/Users/user/.boto]. See this file and
[https://cloud.google.com/storage/docs/gsutil/commands/config] for more
information about configuring Google Cloud Storage.
Your Google Cloud SDK is configured and ready to use!

* Commands that require authentication will use shibagaki.motoaki.rt@mynavi.jp by default
* Commands will reference project `mynavi-cm-adfair` by default
Run `gcloud help config` to learn how to change individual settings

This gcloud configuration is called [default]. You can create additional configurations if you work with multiple accounts and/or projects.
Run `gcloud topic configurations` to learn more.

Some things to try next:

* Run `gcloud --help` to see the Cloud Platform services you can interact with. And run `gcloud help COMMAND` to get help on any gcloud command.
* Run `gcloud topic --help` to learn about advanced features of the SDK like arg files and output formatting

デフォルトのzone/regionの設定がされていないと言われますが、 Your Google Cloud SDK is configured and ready to use!とも書かれているので、こちらで使えるかどうか試してみます。

bqコマンドの確認

bqコマンドがあるかどうか確認します

1
2
$ which bq
/Users/user/google-cloud-sdk/bin/bq

パスは通っているようです

1
2
$ bq version
This is BigQuery CLI 2.0.62

大丈夫そうですね。

まとめ

Google BigQueryを読み進めていくためにbqコマンドを利用できるようにしました。読み進めていきながら実際にコマンドを実行したいと思います。

JavaScriptでsmooth scrollを実装する(IE対応)

背景

JavaScriptでsmooth scrollを実装してほしいという依頼を受けました。そのサイトはあるASPを利用していて、なにかをインストールするということはできません。

方法を探す

JavaScript初心者のわたしはとりあえずググることにしました。すると、スムーズスクロールを実装しようとしている人なら誰もが見ていそうなページが見つかりました。

素のJavaScriptで実装したかったので、このページに書かれているwindow.scrollToを使う方法でいこうと思います。

実装

ざっと実装してみました。

1
2
3
4
5
6
7
8
9
10
11
const buffer = 50;
const scrollTarget = document.getElementById('scroll_target');
const scrollTrigger = document.getElementById('scroll_trigger');

scrollTrigger.addEventListener('click', function (e) {
e.preventDefault();
const scrollTargetTop = scrollTarget.getBoundingClientRect().top;
const offsetTop = window.pageYOffset;
const top = scrollTargetTop + offsetTop - buffer;
window.scrollTo({ top: top, behavior: 'smooth' });
});

scroll_triggerというidを付与したaタグにスクロールのイベントリスナーをセットします。そのほか、いくつかのブラウザのAPIを利用しています。

element.getBoundingClientRect

ドキュメント

ドキュメントにあるように、width と height 以外のプロパティは、ビューポートの左上を基準としていますので、ページ内の現在の表示箇所によって返ってくる値は異なります。(ビューポートの左上より下にあればプラスの値、上にあればマイナスの値)

window.pageYOffset

ドキュメント

こちらもドキュメントにあるように、現在のビューポートがページの一番上からどれくらいスクロールしているかを表します。pageYOffsetscrollYのエイリアスだそうです。

そして、今この文章を書いていて気付いたのですが、

1
const top = scrollTargetTop + offsetTop - buffer;

は常に同じ値を返すので(当たり前ですか?w)、addEventListenerの中で毎回計算する必要はありませんでした…

window.scrollTo

ドキュメント

scrollToはドキュメントの構文にあるように、2種類の引数をとります。

1
2
window.scrollTo(x-coord, y-coord)
window.scrollTo(options)

今回利用しているのは引数にoptionsをとる方です。optionsはこちらで定義されています。今回はtopbehaviorを指定しています。

IEで動作させたい

現状のままでは、IEで動作しませんでした…IEでも動作させたいので、手を入れていきます。

scrollToの引数を変える

先ほど書いた通りwindow.scrollToの引数は2種類あって、IEはオブジェクトではない方であれば、スクロールするということで、やってみました。

1
window.scrollTo(0, top);

すると、スムーズスクロールではなく、その位置に移動する動作になってしまいました。これでは最低限の役目は果たしているもののイマイチです。

polyfillの導入

今回一番勉強になったのがこちらです。最初に紹介したスムーズスクロールを紹介していたページに記載されていましたが、npm installしていたので見過ごしていました。npm installせずとも利用できました。

polyfillとは?

ドキュメント

MDN Web Docsにはなんでもありますね…ドキュメントにあるように、最近の機能をサポートしていない古いブラウザーで、その機能を使えるようにするためのコードです

polyfillを利用する

https://polyfill.io/v3/url-builder/で利用したい機能のチェックボックスにチェックを入れます。今回はsmoothscrollにチェックを入れます。すると、画面上部にURLが表示されます。copy url to clipboardをクリックしコピーします。

今回のURLは

https://polyfill.io/v3/polyfill.min.js?features=smoothscroll

です。これを<script>で読み込みます。

1
<script src="https://polyfill.io/v3/polyfill.min.js?features=smoothscroll"></script>

この1行を一番最初に書いたJavaScriptの上に追加します。そしてIEで試してみると、スムーズスクロールできていました。

まとめ

JavaScript初心者のわたしがスムーズスクロールを実装するまでを記しました。JavaScript初心者はまずブラウザAPIを正しく理解することが重要だと思いました。MDN Web Docsのドキュメントをちゃんと読むべきですね。

JavaScriptの言語仕様を学ぶのであれば、JavaScript Primerを読みましょう。この本はとてもわかりやすく書かれていて、プログラミングの用語についてもわかりやすく説明してくれています(プリミティブ、リテラルなど)

IEで動作しない場合はpolyfillを最初に調べましょう。1行足すだけでIE対応ができるかもしれません。

参考図書

JavaScript Primer

RailsでAuthorizationヘッダを用いた認証を実装する

背景

仕事でAPIの実装を行うことになりました。ログイン後に発行するtokenでユーザー認証が必要ということで、どういった方法があるかを調べてみました。Authorizationヘッダを用いることが多いのは知っていたので調べてみることにしました。

Authorizationヘッダについて

Authorizationの仕様についてはしっかりと理解していなかったので改めて調べてみました。

Authorization - HTTP | MDN

このサイトに記載されているように、
Authorization: <type> <credentials>
が構文のようです。以前、 Authorization: <credentials>という実装をしていましたが、こちらは仕様に準拠していないことになります。

typeについて

Typeにはいろんな種類があるようです。個人的によく見るのは Bearerです。OAuth認証で利用したりしました。
先ほどのMDNの中にtypeの一覧へのリンクがありました。
Hypertext Transfer Protocol (HTTP) Authentication Scheme Registry

TypeがBasicだった場合、credentialsをBase64でデコードする必要があったりして、少し手間がかかりそうな印象です。

Bearerを試してみる

まずは簡単にできそうなBearerを試してみます。
API側では、authenticate_or_request_with_http_tokenメソッドを用います。

1
2
3
4
5
6
def authenticate
authenticate_or_request_with_http_token do |token, _options|
@user = User.find_by_token token
head status: 401 if @user.blank?
end
end

クライアント側はcurlを使います

1
2
$ curl -H 'Authorization: Bearer xxx_credentials' https://api_endpoint
HTTP Token: Access denied.

認証が失敗してしまいました…
ブロック内でbinding.pryを呼び出してもスルーされることから、ブロックに到達する前に認証に失敗しているようです。

いろいろ調べてみると、以下のサイトが見つかりました。

週刊Railsウォッチ(20191118前編)ActiveJob引数のログ抑制、RailsガイドProプランお試し、ファイルアップロードのレジュームgemほか|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

BearerだけではなくTokenでも良いようなので、Tokenを試してみます

1
$ curl -H 'Authorization: Token xxx_credentials' https://api_endpoint

これだとよくわからないので、レスポンスヘッダを表示してみます

1
2
3
4
5
6
7
8
9
10
11
$ curl -D - -H 'Authorization: Token xxx_credentials' https://api_endpoint
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"d396de9e73b218ac6b8f0de7967e82bc"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 0f90b919-e9f6-42ee-a76a-96930fbfdff2
X-Runtime: 0.064172
Transfer-Encoding: chunked

認証成功しました!

ということはTokenではよくてBearerはダメということですね。原因を調査するためにauthenticate_or_request_with_http_tokenを調べてみます。
該当のバージョンでgithubを探すと該当箇所が見つかりました。

rails/http_authentication.rb at 4-2-stable · rails/rails · GitHub

なんと、Bearerでは引っかからないようになっています…
Tokenだと認証可能ですが、Tokenはの定義にはありません…

Basicを試す

こうなったら一般的なBasicを試してみます。credentialsの部分はユーザー名:パスワードになりますが、今回ユーザー名がないので:パスワードとなり、パスワードの部分にcredentialsを指定します。

API側ではauthenticate_or_request_with_http_basicを利用します

1
2
3
4
5
6
def authenticate
authenticate_or_request_with_http_basic do |_user, password|
@user = User.find_by_token password
head status: 401 if @user.blank?
end
end

クライアント側はBase64.encode64(‘:xxx_credentials’)でBase64エンコードした文字列を準備します。(先頭の:を忘れない)

1
2
$ curl -H 'Authorization: Basic base64_encode_credentials' https://api_endpoint 
HTTP/1.1 200 OK

うまく認証されました!

まとめ

認証はセキュリティ的に重要な箇所になるので、慎重に実装しましょう。
AuthorizationヘッダなどHTTPの仕様をしっかり理解してからどの方法を使うかを検討しましょう。

おまけ

今回クライアント側としてcurlを使いました。有用なオプションがいっぱいありますね。以下にまとめたいと思います。

  • -D (–dump-header)

    HTTPレスポンスヘッダを表示します。標準出力に出力するには-を指定します。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ curl -D - https://www.google.com
    HTTP/2 200
    date: Wed, 16 Sep 2020 17:16:16 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    (省略)
  • -o

    レスポンスボディの出力先を指定します。不要な場合に/dev/nullを指定したりします。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    $ curl -D - -o /dev/null https://www.google.com
    % Total % Received % Xferd Average Speed Time Time Time Current
    Dload Upload Total Spent Left Speed
    0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0HTTP/2 200
    date: Wed, 16 Sep 2020 17:19:47 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    x-frame-options: SAMEORIGIN
    set-cookie: 1P_JAR=2020-09-16-17; expires=Fri, 16-Oct-2020 17:19:47 GMT; path=/; domain=.google.com; Secure
    set-cookie: NID=204=rwcy-xyJUKkXMHIec1I-ETPuB_hzV-mbxnl5ot0DhU2q2B-I-aRIVHKkHFyKvsqESZqjRXQFxn7YjzjmvTkflbrfzePsc87d7F7GFELqNbXvPvATIo2v6EaeaHzIXrsoAL4LRKd20uniGfFR6dN8pq3gB4O4SPDzjYd7EJvWLaA; expires=Thu, 18-Mar-2021 17:19:47 GMT; path=/; domain=.google.com; HttpOnly
    alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
    accept-ranges: none
    vary: Accept-Encoding

    100 13141 0 13141 0 0 62279 0 --:--:-- --:--:-- --:--:-- 62279
  • -s

    進捗を表示しないようにします。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ curl -D - -o /dev/null -s https://www.google.com
    HTTP/2 200
    date: Wed, 16 Sep 2020 17:21:07 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    x-frame-options: SAMEORIGIN
    set-cookie: 1P_JAR=2020-09-16-17; expires=Fri, 16-Oct-2020 17:21:07 GMT; path=/; domain=.google.com; Secure
    set-cookie: NID=204=Mdp8loY0lxxV_Kf-YoGRq3db-HvCsX81ffRzePmxfFeT6gb2A72Ijy8AoG5PHsjGj962rqODoZ0VXn9ZMen5MPFGnUww7qnmuexWFLC0JGlyR07K1iIO3iAub_CFUzdjZogChUYvm1Qo9yxyzlm2pb0A2i44va50JOIBi-8qrDE; expires=Thu, 18-Mar-2021 17:21:07 GMT; path=/; domain=.google.com; HttpOnly
    alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
    accept-ranges: none
    vary: Accept-Encoding

    ヘッダだけ欲しい場合はこれで良いですね。

  • -X

    HTTPメソッドを指定します。指定しない場合はGETになります。

    例:

    1
    2
    3
    4
    5
    6
    7
    $ curl -X POST https://www.google.com
    <!DOCTYPE html>
    <html lang=en>
    <meta charset=utf-8>
    <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
    <title>Error 405 (Method Not Allowed)!!1</title>
    (省略)

    405が返ってきてますね…POSTには対応してないようです

  • -F(–form)

    フォームの値を指定します。key=valueという形式で指定します

    例:

    1
    $ curl -X POST -F 'username=user' -F 'password=password' https://somewhere_login_url

他にもいろいろあるようですが、また何かわかったら追記します。

ECSでRailsのassetsを配信する方法

これまでのまとめ

今までGitHub Actionsを用いてECSデプロイする方法を調査してきました。そして、なんとかECSにデプロイできるところまできました。

今回は、デプロイできるようになったものの、アセットが表示されなかったときの対応方法をまとめたいと思います。

状況の確認

ECSは起動タイプにFargateを選択していました。ですのでコンテナの状況はCloudWatchに流れてくるログからのみ知ることができました。

ログにはアセットが見つからないと言う情報だけしかありませんでした。最初はアセットのプリコンパイルが失敗しているのかと思い、コンテナ作成時にプリコンパイルしたり、ECSで起動時にプリコンパイルしたりしましたが状況は変わらずでした。

起動タイプEC2で試す

やはりこういうときはコンテナにアクセスできた方がデバッグが早いです。面倒でしたが起動タイプEC2で起動し、コンテナにアクセスしました。

アセット類はRailsコンテナ内に存在していました。

原因の調査

この時点ではまだ良くわからなかったので、アプリケーションのURLではなく、アセット単体のURLにアクセスしてみました。

するとnginxが404を返してきます。nginxが404を返してくる、そもそもupstreamのRailsにリクエストをフォワードしていないのでは?と思ってnginxの設定を調べてみると、nginxの設定はnginxとRailsが一つのサーバで動作していた時のままでした。

  • nginxのDocumentRootはRAILS_ROOT/public
  • locationの設定もそのまま
  • アセットはnginxが処理し、それ以外はupstreamのRailsにフォワード

これではアセットが表示されないのも当然です。nginxとRailsを別コンテナで動かすという単一責務の法則に従ったことで、以前の設定が使えなくなっていました。

対応

まず、全てのアクセスをupstreamにフォワードするようnginxの設定を変更します。locationの優先順位がちゃんと理解できていなかったことを思い知らされました。nginxについて再度学びたいと思います(後日記載します)。

nginxの設定を変更すると、エラー表示がnginxからRailsに変わりました。これでリクエストがRailsにフォワードされていることがわかります。

Railsのエラーメッセージを確認すると、ActionController::RoutingErrorとなっていて、アセットファイルへのrouteがないと言っています。

原因を確認すると、静的ファイルの配信設定ができていないようでした

1
2
3
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?

config/environments/下のファイルに上記のように記載されていたので、タスク定義ファイルに

1
2
3
"environment": [
{ "name": "RAILS_SERVE_STATIC_FILES", "value": "true" }
],

と設定することで、静的ファイルの配信ができるようになりました。

まとめ

nginxとRailsが同じサーバで動作することに慣れてしまっていたので、原因がわかるまで時間がかかってしまいました。わかってしまえばそんなに大したことではないのですが、わかるまでが大変ですね…

今回のことでRailsやnginxの設定がわかっていないことがよくわかりました。nginxの設定、とくにlocationの設定はちゃんと理解しておくべきだと思うので、これから重点的に学びたいと思います。

ndenvからnodenvへ移行する

今更ながらnodenvに移行

JavaScript Primerという本を買って読んでみると、JavaScriptの今までわからなかったところがわかるようになりとてもJavaScriptが楽しくなってきました。(Web版もあります)。

ローカルの開発環境を新しくしようと思い、anyenv経由でインストールしたndenvをアップデートしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ anyenv update
$ anyenv update
Updating 'anyenv'...
| From https://github.com/riywo/anyenv
| e963a69..67d402f master -> origin/master
| * [new tag] v1.1.2 -> v1.1.2
Updating 'anyenv/anyenv-update'...
Updating 'ndenv'...
Updating 'ndenv/ndenv-yarn-install'...
Updating 'ndenv/node-build'...
Updating 'rbenv'...
| From https://github.com/rbenv/rbenv
| c879cb0..0843745 master -> origin/master
Updating 'rbenv/ruby-build'...
| From https://github.com/rbenv/ruby-build
| 69ccbf4..0a5e059 master -> origin/master
| * [new tag] v20200819 -> v20200819
| * [new tag] v20200722 -> v20200722
| * [new tag] v20200727 -> v20200727
Updating 'anyenv manifest directory'...
| From https://github.com/anyenv/anyenv-install
| dcbcfe1..d9791df master -> origin/master

ところが、ndenvのgithubを見てみると

[Deprecated] nodenv is better alternative

と書かれていました。以前はnodenvを使っていて、いつからかndenvになったと記憶しています。結局元に戻ったと言う感じでしょうか。

nodenvのインストール

anyenvを利用してインストールします

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ anyenv install nodenv
/var/folders/mf/_4_k88fj3nqcsgqdz7dzqhy00000gn/T/nodenv.20200828113307.39306 ~
Cloning https://github.com/nodenv/nodenv.git master to nodenv...
Cloning into 'nodenv'...
remote: Enumerating objects: 14, done.
remote: Counting objects: 100% (14/14), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 4017 (delta 2), reused 4 (delta 1), pack-reused 4003
Receiving objects: 100% (4017/4017), 731.96 KiB | 828.00 KiB/s, done.
Resolving deltas: 100% (2633/2633), done.
~
~/.anyenv/envs/nodenv/plugins ~
Cloning https://github.com/nodenv/node-build.git master to node-build...
Cloning into 'node-build'...
remote: Enumerating objects: 95, done.
remote: Counting objects: 100% (95/95), done.
remote: Compressing objects: 100% (61/61), done.
remote: Total 19746 (delta 39), reused 68 (delta 25), pack-reused 19651
Receiving objects: 100% (19746/19746), 3.51 MiB | 776.00 KiB/s, done.
Resolving deltas: 100% (12653/12653), done.
~
~/.anyenv/envs/nodenv/plugins ~
Cloning https://github.com/nodenv/nodenv-vars.git master to nodenv-vars...
Cloning into 'nodenv-vars'...
remote: Enumerating objects: 211, done.
remote: Total 211 (delta 0), reused 0 (delta 0), pack-reused 211
Receiving objects: 100% (211/211), 31.82 KiB | 201.00 KiB/s, done.
Resolving deltas: 100% (76/76), done.
~

Install nodenv succeeded!
Please reload your profile (exec $SHELL -l) or open a new session.

インストール後はシェルを再起動します

1
$ exec $SHELL -l

ndenvのアンインストール

アンインストールもanyenvを利用して行います。

1
2
$ anyenv uninstall ndenv
anyenv: remove /Users/shibagaki/.anyenv/envs/ndenv?

yesと入力してエンターキーを押すとアンインストールされます。

nodeのインストール

nodenvを使ってインストールします。現時点で最新のLTSである12.18.3をインストールします。

1
2
3
4
5
6
7
8
$ nodenv install v12.18.3
node-build: definition not found: v12.18.3

See all available versions with `nodenv install --list'.

If the version you need is missing, try upgrading node-build:

git -C /Users/shibagaki/.anyenv/envs/nodenv/plugins/node-build pull

見つからないと言われてしまったので、一覧表示してみます。

1
2
3
4
5
6
7
8
$ nodenv install --list
...
12.18.1
12.18.2
12.18.3
13.0.0
13.x-dev
...

先頭のvがいらないんですね。

1
2
3
4
5
$ nodenv install 12.18.3
Downloading node-v12.18.3-darwin-x64.tar.gz...
-> https://nodejs.org/dist/v12.18.3/node-v12.18.3-darwin-x64.tar.gz
Installing node-v12.18.3-darwin-x64...
Installed node-v12.18.3-darwin-x64 to /Users/shibagaki/.anyenv/envs/nodenv/versions/12.18.3

インストールされたことを確認します

1
2
3
4
5
6
7
$ nodenv local 12.18.3
$ which node
/Users/user/.anyenv/envs/nodenv/shims/node
$ node -v
v12.18.3
$ npm -v
6.14.6

これで環境を整えることができました。

まとめ

  • ndenvよりはnodenvを利用するようにする
  • nodenvでバージョンを指定する時は先頭のvはいらない
  • バージョン表示時はvがついている

gitのバージョン管理の仕組みを知る その1

きっかけ

今、【夏の翔泳社祭】PDF版電子書籍50%ポイント還元のキャンペーンが行われています。前回のキャンペーンでたまったポイント(前回は書籍購入で50%ポイント還元)があるので、おもしろそうな本があったら買おうと思っていろいろ見てみるとエンジニアのためのGitの教科書[上級編] Git内部の仕組みを理解するを見つけました。

自分のgitのレベルは、gitを使ってプロジェクトを進めることはできますが、なにかイレギュラーなことが起こった時には、ちょっとした緊張が走ってしまうくらいの感じです。ちゃんと理解できていればそうはならないんだと思うので、これを機にgitをより深く学びたいと思います。

本書の構成

構成は、初級、中級、上級の3部構成になっています。全体で80ページ弱のコンパクトな本です。

今回は初級の部分をまとめていきたいと思います。

Gitのバージョン管理の仕組みを知る 〜初級編〜

まとめ

hexoでtwitter cardを記事ごとに変更する

背景

前回、twitter cardの設定を変更してみました。表示されるようになったのですが、すべての記事で同じだとおもしろくありません。

記事ごとの設定はmdファイルのyaml部分に記述しています。titledateupdatedなどと同じように設定できそうな気がします。ちょっと調べてみましょう。

updatedの実現方法を確認する

このサイトでは更新日時を表示するようにしています。前回の記事だと、以下のように設定しています。

1
2
3
4
5
6
7
8
9
---
title: hexoでtwitter cardの設定を変更する
date: 2020-08-07 12:30:00
updated: 2020-08-07 12:30:00
categories:
- テクニカルブログ
tags:
- hexo
--

上記の設定をどうやって取得しているのかthemesディレクトリ以下で探してみます。

1
2
3
4
$ grep -R updated themes
themes/landscape/layout/_partial/article.ejs: <%- partial('post/updated', {class_name: 'article-date', date_format: null}) %>
themes/landscape/layout/_partial/post/updated.ejs:<% if (post.updated) { %>
themes/landscape/layout/_partial/post/updated.ejs: Updated: <time datetime="<%= date_xml(post.updated) %>" itemprop="dateUpdated"><%= date(post.updated, date_format) %></time>

post.updatedとすることで取得できているようです。

twitter cardのURLを指定してみる

記事にtwitter_imageというキーを設定して、実際に変わるか試してみます。こちらの記事で試してみます。

記事の設定にtwitter_imageを追加して、URLを記述します

1
twitter_image: https://book-reviews.blog/images/github_actions_rails_template.png

次にthemes/landscape/layout/_partial/head.ejsのtwitter cardの設定の箇所で、twitter_imageを参照するよう変更します。

1
25   <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: post.twitter_image}) %>

Card validatorで確認すると、そもそも表示されずタイムアウト…
というか、サイト自体も表示できなくなってしまっています。これはエラーが起こっている可能性が高いので、一旦元に戻します。

post.twitter_imageが参照できない原因調査

post.updatedで正しく参照できている場合と、今回はなにが違うのでしょうか。

post.updatedを参照しているのは、themes/landscape/layout/_partial/post/updated.ejsです。そのファイルを読み込んでいるのはthemes/landscape/layout/_partial/article.ejsです。

article.ejsを読み込んでいるファイルを確認します。

1
2
3
4
$ grep -R 'partial/article' themes/
themes/landscape/layout/page.ejs:<%- partial('_partial/article', {post: page, index: false}) %>
themes/landscape/layout/post.ejs:<%- partial('_partial/article', {post: page, index: false}) %>
themes/landscape/source/css/style.styl:@import "_partial/article"

partialメソッドに変数を引き渡していることがわかりました。この記述が必要そうですね。試してみます。

themes/landscape/layout/layout.ejsの1行目を以下のように変更します。

1
<%- partial('_partial/head', {post: page}) %>

再度twitter cardの設定でpost.twitter_imageを参照するように変更し、Card validatorで試してみます。

記事ごとでtwitter cardの画像を変更できた

うまくいきました!

記事にtwitter_imageの設定がない場合はデフォルトでキービジュアルの画像を表示するように修正します。

1
2
3
4
5
6
<% if (post.twitter_image){ %>
<% twitter_image = post.twitter_image %>
<% } else { %>
<% twitter_image = '/css/images/banner.jpg' %>
<% } %>
<%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: twitter_image}) %>

EJSを初めて書きました。概要は知っておいた方が良さそうですね。

まとめ

今回twitter cardを記事ごとに設定することができました。よりよいサイト作りのために、いろいろカスタマイズしていこうと思います。そのためにはhexo, EJSの知識がもうちょっと必要ですね。学んでいきたいと思います。

hexoでtwitter cardの設定を変更する

背景

しばらく使っていなかったtwitterをブログの記事更新の告知に使おうと思って投稿してみました。自分のツイートを見てみると、twitterカードがうまく表示できていませんでした。titleやdescriptionはデフォルトである程度設定してくれているようです。なのでtwitterカードのカスタマイズを行おうと思います。

headタグがあるpartialを探す

OGP関連の記述はheadタグ内にあります。headタグはどのpartialで記述されているのでしょうか。とりあえずthemesディレクトリ配下をgrepしてみました。

1
2
3
4
5
6
7
8
9
10
11
12
$ grep -R twitter themes
themes/landscape/layout/_partial/head.ejs: <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id}) %>
themes/landscape/source/css/_variables.styl:color-twitter = #00aced
themes/landscape/source/css/_partial/article.styl:.article-share-twitter
themes/landscape/source/css/_partial/article.styl: background: color-twitter
themes/landscape/source/css/_partial/article.styl: text-shadow: 0 1px darken(color-twitter, 20%)
Binary file themes/landscape/source/css/fonts/FontAwesome.otf matches
Binary file themes/landscape/source/css/fonts/fontawesome-webfont.ttf matches
themes/landscape/source/js/script.js: '<a href="https://twitter.com/intent/tweet?url=' + encodedUrl + '" class="article-share-twitter" target="_blank" title="Twitter"></a>',
themes/landscape/README.md:twitter:
themes/landscape/README.md:- **twitter** - Twiiter ID
themes/landscape/_config.yml:twitter:

最初に見つかったtheme/landscape/layout/_partial/head.ejsがそれっぽいですね。中身を確認してみます。

1
2
3
4
5
6
23   <title><% if (title){ %><%= title %> | <% } %><%= config.title %></title>
24 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
25 <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id}) %>
26 <% if (theme.rss){ %>
27 <link rel="alternate" href="<%= url_for(theme.rss) %>" title="<%= config.title %>" type="application/atom+xml">
28 <% } %>

実際に出力されるHTMLを見ると、25行目の1行でOGP関連のタグを全て出力していることがわかります。カスタマイズするにはopen_graphというヘルパメソッド?がなにをしているのかを理解する必要がありそうです。メソッドがどこで定義されているかを確認します。

open_graphメソッド

open_graphメソッドはどこで定義されているかを確認します。
hexoアプリケーションのTOPでgrepしてみます。

1
2
3
$ grep -R open_graph .
./themes/landscape/layout/_partial/head.ejs: <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: '/css/images/banner.jpg', twitter_site: '@motoa'}) %>
./node_modules/hexo/lib/plugins/helper/index.js: helper.register('open_graph', require('./open_graph'));

1行目は先ほどのpartialですね。2行目が定義箇所っぽいですね。該当のファイルの中身を見てみます。

1
2
3
4
5
6
43   helper.register('list_tags', require('./list_tags'));
44 helper.register('list_posts', require('./list_posts'));
45
46 helper.register('open_graph', require('./open_graph'));
47
48 helper.register('number_format', require('./number_format'));

正確には理解できてないですが、open_graphというhelperを登録しているような感じでしょうか。helperの処理はrequire('./open_graph')の戻り値なのでしょう。同じディレクトリにあるopen_graph.jsを見てみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
118   result += meta('twitter:card', twitterCard);
119 result += meta('twitter:title', title);
120 if (description) {
121 result += meta('twitter:description', description, false);
122 }
123
124 if (images.length) {
125 result += meta('twitter:image', images[0], false);
126 }
127
128 if (options.twitter_id) {
129 let twitterId = options.twitter_id;
130 if (twitterId[0] !== '@') twitterId = `@${twitterId}`;
131
132 result += meta('twitter:creator', twitterId);
133 }
134
135 if (options.twitter_site) {
136 result += meta('twitter:site', options.twitter_site, false);
137 }

twitter関連の設定をしている箇所はこんな感じでした。twitterカードの種類(summary, summary_large_image)の設定はtwitterCardという変数で設定されているようです。twitterCardという変数はどこで定義されているのか確認します。

1
42   const twitterCard = options.twitter_card || 'summary';

変数ではなく定数でした。options.twitter_cardが指定されていればそれを、指定されていなければsummaryになるということですね。デフォルトはsummaryになるので、なにも設定していない現在はsummaryになっています。

では、optionsを確認します。

1
30 function openGraphHelper(options = {}) {

optionsはこの関数の引数でした。ということは、open_graphメソッドの引数のハッシュにtwitter_cardというキーでtwitterカードの種類を設定すればできそうですね。

試してみる

theme/landscape/layout/_partial/head.ejsのopen_graphの呼び出しを以下のように変更してみます。

1
<%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: '/css/images/banner.jpg'}) %>

twitter_cardsummary_large_imageに指定しました。またその際に表示する画像をimageで指定しています。今回は試しにキービジュアルの画像を指定しています。

確認

twitter card validatorを利用します。TOPページのURLを入力し、Preview cardをクリックします。

twitter card validatorの結果

summary_large_imageで表示できました!

まとめ

hexoのOGP関連の設定をカスタマイズしてみました。今回の設定だと全てのtwitter:imageがキービジュアルの画像になってしまうので、次回は記事ごとにtwitter:imageを変更できるようにしてみたいと思います。