Contents
- 1 Astroについて
- 2 環境
- 3 サーバの構築
- 4 サンプルシステムの開発
- 5 ディレクリ構造・ファイル構成
- 6 SSGとSSRの使い分け
- 7 astroファイルの構文
- 8 astroコマンド
- 9 設定ファイル
- 10 開発環境の準備
- 11 UIライブラリの使用
Astroについて
AstroはJavaScript製のSSGツールであり、以下の機能を持つ
- SSRでも動作可能で、HTMLコンポーネント単位でSSR・SSGの切り替えが可能(アイランドアーキテクチャ)
※2023年1月リリースのバージョン2から対応。ハイブリッドレンダリングと呼ぶ - サーバサイドでのルーティング
- テンプレートエンジンとして様々な製品(React、Preact、Svelte、Vue、SolidJS、AlpineJS、Lit)を使用可能で、自前のテンプレートエンジンもある
- JavaScript / TypeScriptを使用可能
- SSR時のHTTPデーモン実行環境としてDenoを利用可能であるが開発やSSGビルドではDenoを使用できない
環境
AWS EC2上に構築する。
本番環境については付加状況等を考慮してスペックを決める必要がある。今回はテスト用の環境として以下を使用した。
- OS
Amazon Linux 2 (amzn2-ami-kernel-5.10-hvm-2.0.20221210.1-x86_64-gp2) - インスタンスタイプ
t3a.micro
構成
以下で構成する
- TypeScript
- SSR実行ランタイムは標準のもの(Node.js)
- テンプレートエンジンは標準のもの
TypeScriptは初期状態で使用できるので特別な構築作業は不要である。
Astroに限らずSSGは基本的にスタンドアローンのツールであり、自身のプログラムに組み込み、呼び出して使用するものではない。そのため、通常の使用ではAstroはNode.jsで実行する。
※SSGでもテスト用に内部でサーバ(HTTPデーモン)機能を保持していることがある。
ただし、今回はAstroはSSG兼SSRとして使用するので、SSR用にHTTPデーモンを動作させる。HTTPデーモンについては実行ランタイムが必要となり、標準のNode.jsを使用する。
サーバの構築
AWS上での設定
EC2でサーバ作成後、詳細は割愛するが以下設定が追加で必要
- セキュリティグループで以下を許可
- HTTP (TCP/80)
- HTTPS (TCP/443)
- SSH (TCP/22)
- Elastic IP (固定グローバルIPアドレス)とネットワークインターフェースへの割り当て
- グローバルIPに対してドメイン名でアクセスできるようにする(Elastic IP割り当て時のPublic DNSを使用するか独自ドメインをRoute53等のDNSサービスを使用して割り当てる。Elastic IP割り当て時のPublic DNSが表示されない場合はVPCのDNS設定を変更する。)
- インターネットからアクセス可能にする場合はEC2が属するVPCに以下を追加
- インターネットゲートウェイの作成
- 所属するサブネットについて、作成したインターネットゲートウェイへのデフォルトルート追加
rootになる
各設定を始める前にrootユーザになっておく
sudo su -
firewalld
アクセスコントロールはAWSで行うので、サーバ上では設定しない
SELinux
標準で無効化されている
[root@ip-10-0-13-104 ~]# getenforce
Disabled
その他のLinuxの設定
SSHのポート番号の変更
デフォルトのSSHポート(TCP/22)をインターネットに公開していると不正アクセスをしようとするアクセスが多くなり、アクセスに失敗したとしても不要なログが増えがちである。
ポート番号を変更することである程度軽減できる。
この変更を行う場合、EC2のセキュリティグループの許可設定追加も必要となる。
- 現在はポート指定がないことを確認(デフォルトポートで動作している)
grep "^#Port 22" /etc/ssh/sshd_config
- ログ
[root@ip-10-0-13-104 ~]# grep "^#Port 22" /etc/ssh/sshd_config
#Port 22
- ログ
- 設定変更(ここでは7542番ポートにしているが実際は何でもよい)
sed -i -e "s/^#Port 22/Port 7542/g" /etc/ssh/sshd_config
- 設定が変更されたことを確認
grep "^Port " /etc/ssh/sshd_config
- ログ
[root@ip-10-0-13-104 ~]# grep "^Port " /etc/ssh/sshd_config
Port 7542
- ログ
- sshdを再起動する
systemctl restart sshd
- 再起動後、待ち受けポート番号が変わっていることを確認
ss -lnp | grep sshd
- ログ
[root@ip-10-0-13-104 ~]# ss -lnp | grep sshd
u_dgr UNCONN 0 0 * 19254 * 1515 users:(("sshd",pid=2410,fd=4),("sshd",pid=2393,fd=4))
tcp LISTEN 0 128 0.0.0.0:7542 0.0.0.0:* users:(("sshd",pid=2498,fd=3))
tcp LISTEN 0 128 [::]:7542 [::]:* users:(("sshd",pid=2498,fd=4))
- ログ
タイムゾーンの変更
タイムゾーンは初期状態ではUTCになっているので、JSTに変更する
rm -f /etc/localtime
ln -s /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
ロケールの変更
ロケールがUSになっているので、日本に変更する。
反映には再起動が必要。
localectl set-locale LANG=ja_JP.utf8
reboot
パッケージ最新化
インストール済みの既存パッケージを最新化する
yum update -y
nginxとLet’s Encryptのインストール
astroではTLS機能がないため、nginxでクライアントからのHTTP通信を中継し、astroへ転送するリバースプロキシ構成をとる。
astroはCGIの様にHTTPサーバが都度プロセスを起動するタイプではないので、Apache HTTPDよりもeginxの方が高速で動作して有利である。
Amazon Linuxでは専用リポジトリからインストールする。
- nginxをインストールする
amazon-linux-extras install nginx1 -y
- サービスを起動する
systemctl list-unit-files --type service --no-pager | grep nginx
ログ
systemctl enable nginx
systemctl start nginx
systemctl list-unit-files --type service --no-pager | grep nginx
systemctl status nginx[root@ip-10-0-19-200 ~]# systemctl list-unit-files --type service --no-pager | grep nginx
nginx.service disabled
[root@ip-10-0-19-200 ~]# systemctl enable nginx
Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.
[root@ip-10-0-19-200 ~]# systemctl start nginx
[root@ip-10-0-19-200 ~]# systemctl list-unit-files --type service --no-pager | grep nginx
nginx.service enabled
[root@ip-10-0-19-200 ~]# systemctl status nginx
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)
Active: active (running) since Tue 2023-02-14 22:02:10 JST; 1min 4s ago
Process: 14505 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
Process: 14503 ExecStartPre=/usr/sbin/nginx -t (code=exited, status=0/SUCCESS)
Process: 14500 ExecStartPre=/usr/bin/rm -f /run/nginx.pid (code=exited, status=0/SUCCESS)
Main PID: 14507 (nginx)
CGroup: /system.slice/nginx.service
tq14507 nginx: master process /usr/sbin/nginx
tq14508 nginx: worker process
mq14509 nginx: worker process
Feb 14 22:02:10 ip-10-0-19-200.ap-northeast-1.compute.internal systemd[1]: Starting The nginx HTTP and reverse proxy server...
Feb 14 22:02:10 ip-10-0-19-200.ap-northeast-1.compute.internal nginx[14503]: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
Feb 14 22:02:10 ip-10-0-19-200.ap-northeast-1.compute.internal nginx[14503]: nginx: configuration file /etc/nginx/nginx.conf test is successful
Feb 14 22:02:10 ip-10-0-19-200.ap-northeast-1.compute.internal systemd[1]: Started The nginx HTTP and reverse proxy server. - ブラウザでHTTPアクセスできることを確認する
Welcome to nginx! ページが表示されれば成功
http://<ドメイン名>/
- nginxの設定ファイルを編集し、使用するドメイン名を記載する。
この変更を行わないとLet’s Encryptの証明書インストール時にエラーが出る。
nginxの再起動は不要。
vi /etc/nginx/nginx.conf
以下を変更する(コメントアウトされていない箇所のみ)server_name _;
変更後server_name <ドメイン名>;
- epelリポジトリをインストールする
amazon-linux-extras install epel -y
- Let’s Encrypt用ツールをインストールする
yum install certbot python-certbot-nginx -y
- 証明書を取得・インストールする。
この時登録したメールアドレス宛にニュース等を送ってよいか聞かれるので、不要であればNを入力する。
certbot run \
--nginx \
--agree-tos \
--email <メールアドレス> \
--domain <ドメイン名> -
ブラウザでHTTPSアクセスできることを確認する
Welcome to nginx! ページが表示されれば成功https://<ドメイン名>/
- nginxの設定ファイルを編集し、リバースプロキシ設定を記載する。
vi /etc/nginx/nginx.conf
以下を追記する(HTTPS用のserverディレクティブ内、ssl_dhparamの後くらいに追記する)location / {
proxy_pass http://127.0.0.1:3000/;
} - 設定ファイルの文法チェックを行う
nginx -t
結果nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful - nginxを再起動する
systemctl restart nginx
systemctl status nginx
astroのインストール
Node.js環境のインストール
- nvm(Node.js自体のバージョン管理プログラム)をインストールする
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
- ホームディレクトリからシステムディレクトリに移動する
mv /root/.nvm /usr/local/nvm
- nvmからnpmをインストールする
※Amazon Linux 2はNPMバージョン16までしか対応していないのでバージョン指定することcd /usr/local/nvm
. nvm.sh
nvm install 16 - インストールできたことを確認数
node -e "console.log('Running Node.js ' + process.version)"
- 実行ファイルのエイリアスを作成する
ln -s `which npm` /usr/local/nvm/alias/npm
ln -s `which node` /usr/local/nvm/alias/node - 実行パス用の環境変数をログイン時に設定するようにする
echo "" >> /etc/bashrc
echo "# NODE ENVIRONMENT VARIABLE" >> /etc/bashrc
echo "export PATH=/usr/local/nvm/alias:\$PATH" >> /etc/bashrc - 現在のログインにおいても環境変数を適用する
export PATH=/usr/local/nvm/alias:$PATH
astroパッケージのインストール
以降ではastroのインストールと共にastroのプロジェクトを作成する。新規にプロジェクトを作成するたびに行う必要がある。
今回はプロジェクト名をastro-testとした。
- インストール用のディレクトリを作成する
mkdir /usr/local/astro-test
cd /usr/local/astro-test - astroのインストールとプロジェクト作成を行う
途中プロジェクトのパス等を対話形式で聞かれるので入力すること。
以下ではパスは”.”(カレントディレクトリ)とし、その他はデフォルトとしている。
npm create astro@latest -y
ログ
[root@ip-10-0-19-200 astro-test]# npm create astro@latest -y
lqqqqqk Houston:
x ? ? ? Time to build a sweet new website.
mqqqqqj
astro v2.0.10 Launch sequence initiated.
dir Where should we create your new project?
.
tmpl How would you like to start your new project?
Include sample files
? Template copied
deps Install dependencies?
Yes
? Dependencies installed
git Initialize a new git repository?
Yes
? Git initialized
ts Do you plan to write TypeScript?
Yes
use How strict should TypeScript be?
Strict
? TypeScript customized
next Liftoff confirmed. Explore your project!
Run npm run dev to start the dev server. CTRL+C to stop.
Add frameworks like react or tailwind using astro add.
Stuck? Join us at https://astro.build/chat
lqqqqqk Houston:
x ? ? ? Good luck out there, astronaut! ?
mqqqqqj
npm notice
npm notice New major version of npm available! 8.19.3 -> 9.4.2
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.4.2
npm notice Run npm install -g npm@9.4.2 to update!
npm notice - 実行ファイルのエイリアスを作成する
ln -s `pwd`/node_modules/astro/astro.js /usr/local/nvm/alias/astro
- 調査用データをastroの運営元に送信しないようにする
astro telemetry disable
- 開発用のHTTPデーモンを起動する
astro dev
- ブラウザでHTTPSアクセスし、Welcome to Astroページが表示されることを確認する。
インストール完了後、srcディレクトリ内のユーザプログラム相当のサンプルをいろいろ変更することでブラウザでも変更を確認できる。
Node.jsアダプタのインストール
SSRモードで動作させる際にサーバープログラムを動作させるランタイムが必要になる。Node.jsであれば、Node.js自体はインストール不要であるが、astroとNode.jsを連携させるアダプタをインストールしなければならない。
参考:https://docs.astro.build/ja/guides/integrations-guide/node/
- アダプタをインストールする。
-yオプションをつけない場合は対話形式でnodeのインストールで間違いがないか、設定ファイルに変更を加えても問題ないか問われるので、両方そのままエンターを押して進める。
astro add node -y
- 設定ファイルを書き換える
cat > astro.config.mjs << EOT
import { defineConfig } from 'astro/config';
import node from "@astrojs/node";
export default defineConfig({
output: "server",
adapter: node({
mode: 'standalone'
})
});
EOT - ビルドできることを確認する
astro build
ログ
[root@ip-10-0-19-200 astro-test]# astro build
05:34:47 PM [content] No content directory found. Skipping type generation.
05:34:47 PM [build] output target: server
05:34:47 PM [build] deploy adapter: @astrojs/node
05:34:47 PM [build] Collecting build info...
05:34:47 PM [build] Completed in 113ms.
05:34:47 PM [build] Building server entrypoints...
05:34:53 PM [build] Completed in 6.00s.
finalizing server assets
05:34:53 PM [build] Rearranging server assets...
05:34:53 PM [build] Server built in 6.13s
05:34:53 PM [build] Complete! - サーバを起動させる
node ./dist/server/entry.mjs
- ブラウザでアクセスできるか確認する
denoアダプタのインストール
以下は今回は実行不要である。
SSR時のHTTPデーモン実行環境としてDenoを利用可能である。
ただし、開発やSSGビルドではDenoを使用できないので、パッケージ管理の簡単さや便利なDeno標準ライブラリ等の恩恵を受けられない。
セキュリティ向上(サンドボックス機能)やパフォーマンス面での恩恵はそれほど多くないので、denoを使用するメリットは大きくない。
反対に、node.jsとdenoの両方で実行可能なコード開発や、開発環境とステージング・本番環境の際によるトラブルシューティング等による複雑さ向上を考えれば、現時点でdenoを使用することはお勧めできない。
なお、astroのメイン実行ファイルである、astro.jsをdenoで実行してみたが正常に動作しなかった。
※以下で実行した
nodeでしか使用できない変数をDeno用に変更する
sed -i -e "s/process/Deno/g" node_modules/astro/astro.js
denoで実行する
deno run --allow-read --allow-net /usr/local/astro-test/node_modules/astro/astro.js dev
プロセスがすぐに終了してしまう。build等も同じである。
導入する場合は以下の手順を実施する。
- 参考:https://docs.astro.build/ja/guides/deploy/deno/
- 参考:https://docs.astro.build/ja/guides/integrations-guide/deno/
- astro様にdenoプラグインをインストールする
astro.js add deno
- package.jsonに起動用のコマンドを追加する
※プロジェクトディレクトリのトップで行うvi package.json
preview部分を以下のように変更する"preview": "deno run --allow-net --allow-read --allow-env ./dist/server/entry.mjs",
移行、ビルドするごとにdist/server/entry.mjsファイルが生成され、このファイル中にastroファイルに定義したテンプレートやスクリプト等の内容が記述される(srcディレクトリ配下のファイルはSSR用であっても実行時には不要になる)。
なお、denoで実行する場合、標準では8085番ポートで起動する。
サンプルシステムの開発
一般的なWebサイト開発に必要な機能を組み込んだサンプルシステムを開発する。
以下の機能を組み込む
- SSG / SSRの混在
- HTMLをコンポーネントに分割
- SSGページ中にSSRコンポーネントの挿入
- ダイナミックルーティング
- 外部のTypeScriptファイルに処理を分割してインポート
- Web APIの呼び出し
- ORMを使用してのMySQLへの接続
- ログイン・セッション機能
- 画面からのデータの登録・更新
上記を踏まえて開発するのは下記である
- 共通ヘッダーコンポーネント(SSG)
- ユーザ名表示コンポーネント(SSR)
- トップページ(SSG)
- ユーティリティ TSファイル
- ログインAPI
- ログインページ(SSG)
- 非編集用コンテンツ取得API(内部向け)
- 非編集用コンテンツページA (SSG・ダイナミック)
- 非編集用コンテンツページB (SSG・ダイナミック)
- コンテンツ編集ページ(SSR)※ログインが必要
- 編集用コンテンツ取得API
- 編集用コンテンツ保存API(外部向け)
前準備
- サンプルファイルを削除する
rm -rf src/*
mkdir src/pages - コンポーネント用のディレクトリを作成する
mkdir src/components
- コンテンツページ用のディレクトリを作成する
mkdir src/pages/contents
- API用ディレクトリを作成する
mkdir -p src/pages/api/public
mkdir -p src/pages/api/private - 共通ライブラリ用ディレクトリを作成する
mkdir src/lib
- Node.jsの標準ライブラリの型情報をインストールする
npm install @types/node
このモジュールをインストールしないと、Buffer使用時に以下のエラーが出る。Error: Cannot find name 'Buffer'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.
- ハッシュ関数ライブラリをインストールする
npm install fast-sha256
最初はcrypto-js(@types/crypto-js)を使用しようとしたが、下記のようなエラーが実行時に出たので諦めたcrypt-js doesn't appear to be written in CJS, but also doesn't appear to be a valid ES module (i.e. it doesn't have "type": "module" or an .mjs extension for the entry point). Please contact the package author to fix.
error Failed to resolve entry for package "/usr/local/astro-test/node_modules/@types/crypto-js". The package may have incorrect main/module/exports specified in its package.json. - JWTライブラリをインストールする(JWTとは)
npm install jose
最初はjsonwebtokenをインストールして使用したが、下記のようなエラーがcheck時に出たので諦めた(check時以外は問題ないので、システムの動作として問題ないように思われる)fatal error: all goroutines are asleep - deadlock! goroutine 1 [chan receive]: github.com/evanw/esbuild/internal/helpers.(*ThreadSafeWaitGroup).Wait(...) github.com/evanw/esbuild/internal/helpers/waitgroup.go:36 main.runService.func2() github.com/evanw/esbuild/cmd/esbuild/service.go:127 +0x59 main.runService(0x1) github.com/evanw/esbuild/cmd/esbuild/service.go:181 +0x530 main.main() github.com/evanw/esbuild/cmd/esbuild/main.go:230 +0x21c
- 環境変数ファイルにJWT用の鍵を追記する
- ランダムな文字列を生成する(今回はアルファベットと数字からなる32文字の文字列とした)
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1
- 環境変数ファイルを変更する
vi .env
以下を追記する。※鍵は日付を指定して変更しやすいようにしたJWT_KEY_20230312 = '<鍵の文字列>'
JWT_KEY = ${JWT_KEY_20230312}
- ランダムな文字列を生成する(今回はアルファベットと数字からなる32文字の文字列とした)
- TypeScriptの設定ファイルを変更する(インポート時に絶対パスベースにする)
cat > tsconfig.json << EOT
{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": "src" } }
EOT
MariaDBのインストール
MariaDBを使用するデータベースとしてローカルにインストールする。
- 使用可能なバージョンを確認する
amazon-linux-extras | grep -i maria
- 最新バージョンインストールする
※yumでインストール可能であるが、yumでインストールするとバージョンが古く、mattermost使用時にFULLTEXTインデックスが使用できないとしてエラーになる。amazon-linux-extras install mariadb10.5 -y
- サービスを起動する
systemctl enable mariadb
ログ
systemctl start mariadb
systemctl list-unit-files --type service --no-pager | grep mariadb[root@ip-10-0-13-104 ~]# systemctl enable mariadb
Created symlink from /etc/systemd/system/multi-user.target.wants/mariadb.service to /usr/lib/systemd/system/mariadb.service.
[root@ip-10-0-13-104 ~]# systemctl start mariadb
[root@ip-10-0-13-104 ~]# systemctl list-unit-files --type service --no-pager | grep mariadb
mariadb.service enabled
mariadb@.service disabled - MariaDBに接続する
mysql
- ユーザーを作成する。
パスワードは任意のものに変更してよい。
create user 'astrotest'@'localhost' identified by 'astrotest';
- データベースを作成する
create database astrotest;
- 作成したユーザーにアクセス権限を付与する
grant all privileges on astrotest.* to 'astrotest'@'localhost';
- ログアウトする
exit
- こちらのサイトからログイン情報用パスワードを生成しておく。
変換ルールを1:プレーンTEXT、2:変換不要、3:SHA-256、4:BASE64とする。
例としてpass1、pass2という文字列の場合、それぞれ以下のようになる。
pass1:5sPaWyBmNNfz81htdH/9s2tcZ1dXs4DGpf5cVwxxQ0k=
pass2:G6PRbpiBlZ+MmpdihU9yxuYyHN1ENYoQpOk5AzEX6rk= - 作成したユーザでログインできるか確認する
mysql -u astrotest -pastrotest astrotest
- ログイン情報用テーブルを作成する
create table User ( Id int primary key auto_increment, UserId varchar(128) not null unique, Password char(44) not null comment 'hashed value', LoginAt datetime comment 'login time' ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- ログイン情報用データを挿入する
insert into User (UserId,Password) values ('usera','5sPaWyBmNNfz81htdH/9s2tcZ1dXs4DGpf5cVwxxQ0k=');
insert into User (UserId,Password) values ('userb','G6PRbpiBlZ+MmpdihU9yxuYyHN1ENYoQpOk5AzEX6rk='); - 挿入データを確認する
select * from User;
- コンテンツ用テーブルを作成する
create table Contents (
Id int primary key auto_increment,
Name varchar(128) not null,
Value varchar(128) not null,
UpdatedAt datetime not null default current_timestamp on update current_timestamp comment 'update time'
) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - コンテンツ用データを挿入する
insert into Contents (Name,Value) values ('AAA','111'),('BBB','222');
- 挿入データを確認する
select * from Contents;
- 編集用コンテンツ用テーブルを作成する
create table Article ( Title text not null, Body text not null, EditorId int not null unique, UpdateAt datetime not null default current_timestamp on update current_timestamp comment 'update time', foreign key (EditorId) references User(Id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
- 編集用コンテンツ用データを挿入する
insert into Article (Title,Body,EditorId) values ('Test','Test',1);
- 挿入データを確認する
select * from Article;
- ログアウトする
exit
ORM(Prisma)のインストール
ORMとしてはPrismaを利用する。TypeScriptで使用できる主なORMとしてはSequelize、TypeORM、Prismaがある。
SQLのパフォーマンスチューニングやトラブルシューティングがしやすいのでクエリビルダーによる自動SQL生成ではなく、生のSQLを使用したい。ただし、クラス等を使用しての型付け・オブジェクト化をselect・insert・update全てでサポートしている必要がある。
※ORM(Object Relation Mapper)とはデータベースのデータとプログラミング言語上のオブジェクトを自動で対応させて扱えるようにするデータベース操作用ライブラリである。
- prismaをインストールする
npm install prisma
- prismaの設定ファイル類を生成する
./node_modules/.bin/prisma init
- 設定ファイルを書き換える
cat > prisma/schema.prisma << EOT generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } EOT
- 環境用の設定ファイルを編集する
vi .env
以下を末尾に張り付けるDATABASE_URL = 'mysql://astrotest:astrotest@localhost:3306/astrotest'
その際# This was inserted by `prisma init`:以下部分は削除しておく - DBに作成したテーブルから定義情報を抽出して設定に取り込む
./node_modules/.bin/prisma db pull
- 設定ファイルを確認する
cat prisma/schema.prisma
実行例
generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model Article { Title String @db.Text Body String @db.Text EditorId Int @unique(map: "EditorId") UpdateAt DateTime @default(now()) @db.DateTime(0) User User @relation(fields: [EditorId], references: [Id], onUpdate: Restrict, map: "Article_ibfk_1") } model Contents { Id Int @id @default(autoincrement()) Name String @db.VarChar(128) Value String @db.VarChar(128) UpdatedAt DateTime @default(now()) @db.DateTime(0) } model User { Id Int @id @default(autoincrement()) UserId String @unique(map: "UserId") @db.VarChar(128) Password String @db.Char(44) LoginAt DateTime? @db.DateTime(0) Article Article? }
- 以下を末尾に追記する
vi prisma/schema.prisma
追記内容model ArticleAndUser { Title String @db.Text Body String @db.Text UserId String @unique(map: "UserId") @db.VarChar(128) UpdateAt DateTime @db.DateTime(0) }
- 設定ファイルをビルドする
./node_modules/.bin/prisma generate
nginxへのアクセス制御の追加
内部向けAPIについて、外部からアクセスできないようにnginxに設定を追加する
vi /etc/nginx/nginx.conf
以下をlocation /部分の前の箇所に追記する
location /api/private/ {
deny all;
}
再起動する
systemctl restart nginx
共通ヘッダーコンポーネント(SSG)
vi src/components/base.astro
以下を張り付ける
---
export const prerender = true;
export interface Props {
title: string;
}
const { title } = Astro.props;
import Whoareyou from 'components/whoareyou.astro';
---
<!DOCTYPE html>
<html lang="jp">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
</head>
<body>
<a href="/">トップページへ</a><br/>
<Whoareyou /><br/>
<slot />
</body>
</html>
<style is:global> html { font-family: Meiryo, sans-serif; background-color: #A5F720; } </style>
export const prerender = true;部分はこのコンポーネントをSSG対象として宣言するものである。
参考:https://docs.astro.build/en/guides/server-side-rendering/#hybrid-rendering
export interface Props部分はコンポーネントが取る引数の名前と型を定義している。
このコンポーネントを呼び出す別のastroファイルが引数を与える際に型チェックを行う。ただし、TypeScriptの型チェック全般はastroではなく、開発時のVSCodeエディタ上で行われる。
参考:https://docs.astro.build/ja/guides/typescript/#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88props
<slot />部分はこのコンポーネントを呼び出したastroファイルを子要素としてこの位置にはめ込むastro既定の要素である。
参考:https://docs.astro.build/ja/core-concepts/astro-components/#%E3%82%B9%E3%83%AD%E3%83%83%E3%83%88
ユーザ名表示コンポーネント(SSR)
vi src/components/whoareyou.astro
以下を張り付ける
--- const SessionCookieKey = "session"; import type { UserInfo } from 'lib/util'; import { checkLogin } from 'lib/util'; let message = "ログインしていません"; if(Astro.cookies.has(SessionCookieKey)) { const Token: string = Astro.cookies.get(SessionCookieKey).value!; const UserInfo: UserInfo = (await checkLogin(Token))!; if(!UserInfo) return; message = UserInfo.userId+"のIDでログインしています"; } ---
<p>{message}</p>
process.env.JWT_KEY!;部分は環境変数ファイルに定義した定数を読み込むNode.jsの記法である。末尾の ! は、string | undefinedのunion型(定義した型のいずれかが返る)をstring型変数に代入するためのものである。
Astro.cookiesはAstroでCookieにアクセスするための変数である。SSRモードのみで使用できる。
参考:https://docs.astro.build/ja/reference/api-reference/#astrocookies
{message}部分はテンプレートに埋め込んだ変数である。スクリプト部分に同名の変数を定義するかAstro.propsのようなastroファイルに与えられた引数を参照することで、その変数の最終的な値がテンプレートに挿入される。
トップページ(SSG)
rm -f src/pages/index.astro
vi src/pages/index.astro
以下を張り付ける
--- export const prerender = true; import Base from 'components/base.astro'; ---
<Base title="login">
<ul>
<li><a href="/login">ログイン</a></li>
<li><a href="/contents/1">コンテンツ1</a></li>
<li><a href="/contents/2">コンテンツ2</a></li>
<li><a href="/edit">編集</a></li>
</ul>
</Base>
export const prerender = true;部分はこのコンポーネントをSSG対象として宣言するものである。
参考:https://docs.astro.build/en/guides/server-side-rendering/#hybrid-rendering
ユーティリティ TSファイル
別のファイルから呼び出して使用するユーティリティ関数。以下がある。
- クエリストリング(key1=value1&key2=value2)を連想配列に変換する
- Cookie(key1=value1; key2=value2)を連想配列に変換する
- JWTの検証
- JWTの生成
vi src/lib/util.ts
以下を張り付ける
import type { Request } from '@astrojs/webapi/mod'; export type GetArgs = { params: object, request: Request } export type PostArgs = { request: Request } /** * クエリストリングのパース */ export type QueryString = Map<string, string|array>; export function querystringParse(str: string): QueryString { const ResultMap: Map<string, string|array> = new Map(); const KeyValues: Array = str.split("&"); for(const next of KeyValues) { const KeyValueSet: Array = next.split("="); const Key = KeyValueSet[0]; const Value = KeyValueSet[1]==="" ? null : KeyValueSet[1]; if(ResultMap.has(Key)) { const ExistedValue = ResultMap.get(Key); if(typeof ExistedValue === "string") { ResultMap.set(Key, [ExistedValue, Value]); } else { ExistedValue.push(Value); } } else { ResultMap.set(Key, Value); } } return ResultMap; } /** * cookieのパース */ export type Cookie = Map<string, string>; export function cookieParse(str: string): Cookie { const ResultMap: Cookie = new Map<string, string>(); const KeyValues: Array = str.split(";"); for(const next of KeyValues) { const KeyValueSet: Array = next.split("="); const Key = KeyValueSet[0].trim(); const Value = KeyValueSet[1]==="" ? null : KeyValueSet[1].trim(); ResultMap.set(Key, Value); } return ResultMap; } /** * JWTのチェック */ export interface UserInfo { id: number; userId: string; } import * as jose from 'jose'; export async function checkLogin(token: string): Promise<UserInfo|null> { const JWT_KEY = Buffer.from(process.env.JWT_KEY!, "hex"); const Decrepted = await jose.jwtVerify(token, JWT_KEY); try { return Decrepted.payload; } catch(error) { return null; } } /** * JWTの生成 */ export async function getToken(userInfo: UserInfo): Promise<string> { const JWT_KEY = Buffer.from(process.env.JWT_KEY!, "hex"); const jwt = await new jose.SignJWT(userInfo) .setProtectedHeader({ alg: "HS256" }) .setExpirationTime("60s") .sign(JWT_KEY); return jwt; }
process.env.JWT_KEY!;部分は環境変数ファイルに定義した定数を読み込むNode.jsの記法である。末尾の ! は、string | undefinedのunion型(定義した型のいずれかが返る)をstring型変数に代入するためのものである。
ログインAPI
vi src/pages/api/private/login.json.ts
以下を張り付ける
import { PrismaClient } from '@prisma/client'; import type { User } from '@prisma/client'; import type { UserInfo } from 'lib/util'; export async function post({ request }) { const RequestBody = await request.json(); const userId = RequestBody.userId; const password = RequestBody.password; const UserInfo: UserInfo = { id : null, userId : userId, }; const DBClient = new PrismaClient(); const User: User[] = await DBClient.$queryRaw<User[]> `select Id from User where UserId=${userId} and Password=${password}`; if(User.length > 0) { UserInfo.id = User[0].Id; await DBClient.$executeRaw`update User set LoginAt=CURRENT_TIMESTAMP where id=${UserInfo.id}`; } DBClient.$disconnect(); return {body:JSON.stringify(UserInfo)}; }
ファイル拡張子の.json.tsはTypeScriptファイルであることを示している。JSON形式でレスポンスを返すのでastroファイルのようなテンプレートが不要だからである。このAPIにアクセス時は.tsを取り除いたものになる。
同一のimport元に対してimport、import typeで分けているのは型情報のみのインポートの場合は値として使用することがないため、astro check時にimportsNotUsedAsValuesエラーが出てしまうためである。
参考:https://docs.astro.build/ja/guides/typescript/#%E5%9E%8B%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88
export async function post({ request })部分はこのAPIがHTTP POSTメソッドでアクセスされた時の動作を記述する関数を宣言している。宣言していないHTTPメソッドはエラーになる。
ログインページ(SSG)
vi src/pages/login.astro
以下を張り付ける
--- import { QueryString, querystringParse } from 'lib/util'; import sha256 from "fast-sha256"; import type { UserInfo } from 'lib/util'; import { getToken } from 'lib/util'; const SessionCookieKey = "session"; let message = ""; if(Astro.request.method=="POST") { const QueryStringMap: QueryString = querystringParse(await Astro.request.text()); if(QueryStringMap.has("password")) { const Password: string = QueryStringMap.get("password"); const Encoder = new TextEncoder(); const EncodedPassword: Uint8Array = Encoder.encode(Password); const HasedPassword: Uint8Array = sha256(EncodedPassword); const HasedPasswordBase64: string = Buffer.from(HasedPassword).toString('base64'); const UserInfo: UserInfo = await fetch("http://localhost:3000/api/private/login.json", { method: "POST", body: JSON.stringify({ userId : QueryStringMap.get("userId"), password : HasedPasswordBase64 }) }).then((response) => response.json()); if(UserInfo.id==null) { message = "ユーザー情報がありません"; } else { message = "ログインに成功しました"; const Token = await getToken(UserInfo); Astro.cookies.set(SessionCookieKey, Token, { maxAge:60, secure:true, httpOnly:true }); let OriginPath: string|null = Astro.url.searchParams.has("path") ? Astro.url.searchParams.get("path") : null; // 不正なリダイレクト対策 if(OriginPath && OriginPath.charAt(0)!="/") { OriginPath = null; } if(OriginPath) { return Astro.redirect(OriginPath); } } } } import Base from 'components/base.astro'; --- <Base title="login">
<form method="post">
<label for="userId">User ID</label>
<input type="text" name="userId" required="required" />
<label for="password">Password</label>
<input type="password" name="password" required="required" />
<button type="submit">login</button>
</form>
<br/>
<p>{message}</p>
</Base>
import { QueryString, querystringParse } from ‘lib/util’;部分はユーティリティ TSファイルをインポートしているが、拡張子の.tsを省略しているのは、省略しないとエラーが出るためである。
fetch関数はastroで使用できるHTTPアクセスを行う関数である。今回はPOSTメソッドを使用しているが、その他のメソッドも使用できる。
参考:https://docs.astro.build/ja/guides/data-fetching/
Astro.cookiesはAstroでCookieにアクセスするための変数である。SSRモードのみで使用できる。
参考:https://docs.astro.build/ja/reference/api-reference/#astrocookies
{message}部分はテンプレートに埋め込んだ変数である。スクリプト部分に同名の変数を定義するかAstro.propsのようなastroファイルに与えられた引数を参照することで、その変数の最終的な値がテンプレートに挿入される。
let OriginPath以降の処理はログインが必要なページからこのページにリダイレクトされた際に、ログイン成功後に元のページに再度リダイレクトするための処理である。
参考:https://docs.astro.build/ja/guides/server-side-rendering/#astroredirect
非編集用コンテンツ取得API
vi src/pages/api/private/getAllContents.json.ts
以下を張り付ける
import { PrismaClient } from '@prisma/client'; import type { Contents } from '@prisma/client';
import type { GetArgs } from 'lib/util'; export async function get({params, request}: GetArgs) { const DBClient = new PrismaClient(); const Contents: Contents[] = await DBClient.$queryRaw<Contents[]>`select * from Contents`; DBClient.$disconnect(); return {body:JSON.stringify({ Contents: Contents })}; }
ファイル拡張子の.json.tsはTypeScriptファイルであることを示している。JSON形式でレスポンスを返すのでastroファイルのようなテンプレートが不要だからである。このAPIにアクセス時は.tsを取り除いたものになる。
同一のimport元に対してimport、import typeで分けているのは型情報のみのインポートの場合は値として使用することがないため、astro check時にimportsNotUsedAsValuesエラーが出てしまうためである。
参考:https://docs.astro.build/ja/guides/typescript/#%E5%9E%8B%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88
export async function get({params, request})部分はHTTP GETでの動作を定義している。
参考:https://docs.astro.build/ja/core-concepts/endpoints/#http%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89
非編集用コンテンツページA・B (SSG・ダイナミック)
vi src/pages/contents/[id].astro
以下を張り付ける
--- import type { Contents } from '@prisma/client'; export const prerender = true; export interface ContentsParams { params : { id : string }; props : { Name : string, Value : string }; } export async function getStaticPaths() { let apiResponse = await fetch("http://localhost:3000/api/private/getAllContents.json"). then((response) => response.json()); const Contents : Array<Contents> = apiResponse.Contents; const Paths: Array<{params:{id:string},props:{Name:string,Value:string}}> = []; for(const next of Contents) { Paths.push({ params:{ id:""+next.Id }, props:{ Name:next.Name, Value:next.Value} }); } return Paths; } const Id = Astro.params.id; // 使用しない import Base from 'components/base.astro'; ---
<Base title={"コンテンツページ:"+Astro.props.Name}>
<h1>{Astro.props.Name}</h1>
<p>{Astro.props.Value}</p>
</Base>
ファイル名を[]で囲っているのは、ダイナミックルーティングであることを示すためである。
参考:https://docs.astro.build/ja/core-concepts/endpoints/#params%E3%81%A8%E5%8B%95%E7%9A%84%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0
export const prerender = true;部分はこのコンポーネントをSSG対象として宣言するものである。
参考:https://docs.astro.build/en/guides/server-side-rendering/#hybrid-rendering
export async function getStaticPaths()部分はダイナミックルーティングのコンポーネントにおいて、SSGを使用する際にどのURLを生成すべきか、生成する際に与えるパラメータを定義するものである。
ファイル名で[]で囲った部分の変数名を必ずparams属性で定義する必要がある。props属性は任意である。今回はデータベースで取得した値を渡すことで、コンポーネントの各実態を生成する際に個別のデータベース接続を省略できるようにした。実態数が多いとメモリを圧迫するので個別にDB接続しなければならないし、そもそもフロントエンドの役割にであるastroファイルでDB接続等のサーバサイドのロジックを記述するべきではない。
なお、getStaticPaths関数はビルド時は最初の1度のみ評価されるので、その後の個別の実態ページの生成時には評価されない。従って、上記の例ではDB接続は1度のみしかされない。
参考:https://docs.astro.build/ja/reference/api-reference/#getstaticpaths
fetch関数はastroで使用できるHTTPアクセスを行う関数である。今回はGETメソッドを使用しているが、POSTメソッド等も使用できる。
参考:https://docs.astro.build/ja/guides/data-fetching/
{Astro.props.Name}、{Astro.props.Value}部分はテンプレートに埋め込んだ変数である。スクリプト部分に同名の変数を定義するかAstro.propsのようなastroファイルに与えられた引数を参照することで、その変数の最終的な値がテンプレートに挿入される。
編集用コンテンツ取得API(内部向け)
vi src/pages/api/private/getArticle.json.ts
以下を張り付ける
import { PrismaClient } from '@prisma/client'; import type { ArticleAndUser } from '@prisma/client';
import type { GetArgs } from 'lib/util'; expfget({params, request}: GetArgs) { const DBClient = new PrismaClient(); const Article: ArticleAndUser[] = await DBClient.$queryRaw<ArticleAndUser[]> `select Article.Title, Article.Body, Article.UpdateAt, User.UserId from Article inner join User on User.Id=Article.EditorId limit 1`; DBClient.$disconnect(); return {body:JSON.stringify( Article.length > 0 ? Article[0] : {} )}; }
ファイル拡張子の.json.tsはTypeScriptファイルであることを示している。JSON形式でレスポンスを返すのでastroファイルのようなテンプレートが不要だからである。このAPIにアクセス時は.tsを取り除いたものになる。
同一のimport元に対してimport、import typeで分けているのは型情報のみのインポートの場合は値として使用することがないため、astro check時にimportsNotUsedAsValuesエラーが出てしまうためである。
参考:https://docs.astro.build/ja/guides/typescript/#%E5%9E%8B%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88
編集用コンテンツ保存API(外部向け)
vi src/pages/api/public/saveArticle.json.ts
以下を張り付ける
import { PrismaClient } from '@prisma/client'; import { Cookie, cookieParse } from 'lib/util'; import type { UserInfo } from 'lib/util'; import { checkLogin } from 'lib/util';
import type { PostArgs } from 'lib/util'; const SessionCookieKey = "session"; export async function post({ request }: PostArgs) { let UserId: number; const Cookie: Cookie = cookieParse(request.headers.get("Cookie")); // ログインチェック if(Cookie.has(SessionCookieKey)) { const Token: string = Cookie.get(SessionCookieKey)!; const UserInfo: UserInfo = (await checkLogin(Token))!; if(!UserInfo) { return new Response(null, { status: 400, statusText: 'Not login' }); } UserId = UserInfo.id!; } else { return new Response(null, { status: 400, statusText: 'Not login' }); } const RequestBody = await request.json(); const DBClient = new PrismaClient(); await DBClient.$executeRaw `update Article set Title=${RequestBody.Title}, Body=${RequestBody.Body}, EditorId=${UserId}`; DBClient.$disconnect(); return new Response(null, { status: 200 }); }
ファイル拡張子の.json.tsはTypeScriptファイルであることを示している。JSON形式でレスポンスを返すのでastroファイルのようなテンプレートが不要だからである。このAPIにアクセス時は.tsを取り除いたものになる。
なお、実際にはこのAPIはJSONを返さない。
import { Cookie, cookieParse } from ‘lib/util’;部分はユーティリティ TSファイルをインポートしているが、拡張子の.tsを省略しているのは、省略しないとエラーが出るためである。
return new Response部分はAPIのレスポンスを定義している。ステータスコードやステータステキスト、その他の属性が設定できる。
参考:https://docs.astro.build/ja/core-concepts/endpoints/#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88api%E3%83%AB%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0
参考:https://developer.mozilla.org/ja/docs/Web/API/Response
コンテンツ編集ページ(SSR)※ログインが必要
vi src/pages/edit.astro
以下を張り付ける
--- import type { ArticleAndUser } from '@prisma/client'; import type { UserInfo } from 'lib/util'; import { checkLogin } from 'lib/util'; const SessionCookieKey = "session"; // ログインチェック if(Astro.cookies.has(SessionCookieKey)) { const Token: string = Astro.cookies.get(SessionCookieKey).value!; const UserInfo: UserInfo = (await checkLogin(Token))!; if(!UserInfo) { return Astro.redirect('/login?path='+Astro.url.pathname); } } else { return Astro.redirect('/login?path='+Astro.url.pathname); } // メイン処理 const Article: ArticleAndUser = await fetch("http://localhost:3000/api/private/getArticle.json") .then((response) => response.json()); const UpdateAtDate: Date = new Date(Article.UpdateAt); const ParsedDate: string = UpdateAtDate.getFullYear()+"/"+ ((UpdateAtDate.getMonth()+1)<10?("0"+(UpdateAtDate.getMonth()+1)):(UpdateAtDate.getMonth()+1))+"/"+ (UpdateAtDate.getDate()<10?("0"+UpdateAtDate.getDate()):UpdateAtDate.getDate())+" "+ (UpdateAtDate.getHours()<10?("0"+UpdateAtDate.getHours()):UpdateAtDate.getHours())+":"+ (UpdateAtDate.getMinutes()<10?("0"+UpdateAtDate.getMinutes()):UpdateAtDate.getMinutes())+":"+ (UpdateAtDate.getSeconds()<10?("0"+UpdateAtDate.getSeconds()):UpdateAtDate.getSeconds()); import Base from 'components/base.astro'; ---
<Base title="article edit">
<label for="title">タイトル</label>
<input type="text" id="title" name="title" value={Article.Title}/><br/>
<label for="body">本文</label>
<textarea id="body" name="body" rows="20" cols="100">{Article.Body}</textarea>
<p>編集者:{Article.UserId}</p>
<p>編集日時:{ParsedDate}</p>
<button type="button" id="saveButton">保存</button>
<script is:inline>
document.getElementById("saveButton").addEventListener('click', function() { fetch("/api/public/saveArticle.json", { method: "POST", body: JSON.stringify({ Title : document.getElementById("title").value, Body : document.getElementById("body").value }) }).then((response) => { alert(response.ok ? "保存に成功しました" : "保存に失敗しました"); }); }); </script>
</Base>
Astro.cookiesはAstroでCookieにアクセスするための変数である。SSRモードのみで使用できる。
参考:https://docs.astro.build/ja/reference/api-reference/#astrocookies
Astro.redirectはリダイレクトするための処理である。
参考:https://docs.astro.build/ja/guides/server-side-rendering/#astroredirect
script要素のis:inline属性はastroにおいて、スクリプトの実行をサーバで行わずにブラウザで行うように指定するためのものである。これがないと
動作確認
チェックコマンドでプログラムに問題がないかを確認する
astro check
以下のようにerror、wariningが出ていなければ問題ない
astro check
04:53:59 PM [content] No content directory found. Skipping type generation.
? Getting diagnostics for Astro files in /usr/local/astro-test/…
/usr/local/astro-test/src/pages/contents/[id].astro:34:7 Hint: 'Id' is declared but its value is never read.
34 const Id = Astro.params.id; // 使用しない
~~
35 import Base from 'components/base.astro';
Result (6 files):
- 0 errors
- 0 warnings
- 1 hint
次にビルドを行う。動作確認はSSG部分もSSRで動作するのでビルドは不要であるが、今回は確認のため行う。
astro build
以下のようにCompleteで完了していれば問題ない。
05:26:46 PM [content] No content directory found. Skipping type generation.
05:26:46 PM [build] output target: server
05:26:46 PM [build] deploy adapter: @astrojs/node
05:26:46 PM [build] Collecting build info...
05:26:46 PM [build] Completed in 159ms.
05:26:46 PM [build] Building server entrypoints...
05:26:51 PM [build] Completed in 4.77s.
prerendering static routes
Server listening on http://127.0.0.1:3000
? src/pages/index.astro
mq /index.html (+59ms)
? src/pages/contents/[id].astro
tq /contents/1/index.html (+620ms)
mq /contents/2/index.html (+632ms)
Completed in 1.43s.
finalizing server assets
05:26:52 PM [build] Rearranging server assets...
05:26:52 PM [build] Server built in 6.43s
05:26:52 PM [build] Complete!
最後に動作確認用のサーバモードで起動してブラウザでアクセスし、動作確認する。
astro dev
実行すると以下のように出力されてブロックされる。
? astro v2.0.15 started in 73ms
x Local http://localhost:3000/
x Network use --host to expose
Ctrl+Cで終了できる
本番用の動作方法で実行する場合は次のコマンドである。SSGページが静的ファイルを返したり、開発用のJavaScriptが埋め込まれなかったりと多少動作が異なる。
node ./dist/server/entry.mjs
ディレクリ構造・ファイル構成
初期状態
サンプルプロジェクト込みでインストールした場合、astroインストール直後のディレクトリの構造は以下のようになる。
※node_modules配下は除外
- コマンド
find -type d | grep -v "./node_modules/"
- 結果
./.vscode
./public
./src
./src/components
./src/layouts
./src/pages
./node_modules
サンプルプロジェクト込みでインストールした場合、astroインストール直後のファイルの構成は以下のようになる。
※node_modules配下は除外
- コマンド
find -type f | grep -v "./node_modules/"
- 結果
./.gitignore
./.vscode/extensions.json
./.vscode/launch.json
./README.md
./astro.config.mjs
./package.json
./public/favicon.svg
./src/components/Card.astro
./src/env.d.ts
./src/layouts/Layout.astro
./src/pages/index.astro
./tsconfig.json
./package-lock.json
各ディレクトリ・ファイルについて
srcディレクトリ
サイトを構成するテンプレートやその装飾、プログラムが配置されるディレクトリである。
配下に複数のディレクトリが配置されるが、最も重要なのはpagesである。ほかのディレクトリについては任意であり、共通コンポーネントを配置するために使用する。
pagesディレクトリ
astroのルーティングはディレクトリのパスベース(ファイルベースルーティング)であり、このディレクトリ配下のパスがそのままWebサイトのURL上のパスとなる。
astroファイル
参考:https://docs.astro.build/ja/core-concepts/astro-components/
HTMLのテンプレートであり、かつテンプレートのスタイルシート、及びテンプレート中に埋め込んだ変数を解決するためのJavaScript・TypeScriptを記述するファイル。
このファイルには1つのHTMLページを記述することもできるし、HTMLコンポーネントとして記述し、他のastroファイルからインポートして利用することもできる。
HTMLを記述せずにスクリプトのみ記述し、JSON等の別の形式のテキストファイルや画像等のバイナリファイルを生成することもできる。
ただし、astroファイルではHTTP GETメソッドにしか対応していない。
デフォルトのモードであるSSGモードでは、astroファイルは事前にビルドされ、1つのHTMLファイルが生成される。
SSRモードにした場合はastroファイル単位でSSGであることを宣言しない限りデフォルトでSSRとなるが、pagesディレクトリ配下のastroファイルがビルドできた時点でHTMLをクライアントに返却し、残りのコンポーネントはビルド出来次第クライアントに返し、クライアント上でコンポーネントが結合される(HTMLストリーミング)。
.ts、.jsファイル
参考:https://docs.astro.build/ja/core-concepts/endpoints/
API用としてTypeScript / JavaScriptのファイルを利用できる。astroファイルと違い、任意のHTTPメソッド(POST、PUT等)に対応している。
ただし、astroファイルでは標準で使用できるAstroオブジェクトにはアクセスできないので、リクエストヘッダーやcookieのアクセス方法が異なる。
publicディレクトリ
外部から持ち込んだ静的ファイルを配置する。
ビルド時にdistディレクトリにそのままコピーされる。
distディレクトリ
インストール直後には存在しないが、ビルドすることで作成される。
ビルドした静的ファイルが配置され、previewモード等で公開される。
SSGのみ使用の際はastroはHTTPデーモンとして起動しないので、別途Webサーバを用意して公開する。
SSGとSSRの使い分け
SSRを有効にすることでデフォルト動作がSSRとなるが、コンポーネント単位で宣言することでSSGでの動作とすることができる。
※ハイブリッドレンダリング
なお、SSRモード時にSSGとしたいコンポーネントはスクリプト部に以下を追記するだけである。
export const prerender = true;
SSGで使用できる機能
SSRで使用できる機能
SSR(サーバサイドレンダリング)でのみ使用できる機能がある。
動的ルーティング
例えば/book/123や/book/abcのようなパスに対し、1つのastroファイルで内容の異なるHTMLを返すことができる機能。
リクエストヘッダー・ボディの受け取り
リクエストヘッダー等のリクエスト時にクライアントから渡された情報を参照できる。
参考:https://docs.astro.build/ja/core-concepts/endpoints/#request
スクリプトによるレスポンスの生成
HTMLテンプレートによるのではなく、ビルド時に実行されたスクリプトの結果をレスポンスとして返すことができる。
HTTPメソッド
SSGではGETメソッドしか対応していないが、SSRではPOST等の他のメソッドに対応できる。
参考:https://docs.astro.build/ja/core-concepts/endpoints/#http%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89
リダイレクト
動的ルーティングでコンテンツが見つからないときや、ログインチェックで未ログインであった場合等にリダイレクトで別のページに転送することができる。リダイレクトはHTTPの仕組みを使用するので、クライアントによりリダイレクト先へ再度のアクセスがされることになる(サーバ側で代理取得して返すわけではない)。
astroファイルの構文
astroファイルはフロントマター部分とHTMLテンプレート部分に大別され、両者はコードフェンス(—)によって分割される。つまり、以下のようになる。
---
フロントマター
---
HTMLテンプレート
フロントマター部分ではJavaScript / TypeScriptでNPMモジュールを読み込んだり、DBアクセスや、何らかの計算処理等を行うことができる。ここで定義した変数はHTMLテンプレート部分で参照可能になる。
HTMLテンプレート部分では通常のHTMLと同様の記述ができ、更にその中に変数の埋め込みや、別ファイルに分割されたHTMLテンプレートのインポート等も行える。
参考:https://docs.astro.build/ja/core-concepts/astro-syntax/
フロントマター
HTMLテンプレート
ループ処理
配列や連想配列から配列の要素数だけHTML要素を生成する場合、以下のようにする
参考:https://docs.astro.build/ja/core-concepts/astro-components/#%E5%8B%95%E7%9A%84%E3%81%AAhtml
- 配列
map関数のみサポートしており、forEach関数では動作しない。
---
const items = ["犬", "猫", "カモノハシ"];
---
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul> - 連想配列
配列型のみサポートしているので(iterator型も不可)、一旦配列に変換してから配列と同様の方法で実行する必要がある。
---
const items = {key1:"犬", key2:"猫", key3:"カモノハシ"};
---
<ul>
{Array.from(items.keys()).map((item) => (
<li>{item+":"+items[item]}</li>
))}
</ul>
astroコマンド
参考:https://docs.astro.build/ja/reference/cli-reference/
dev
HTTPデーモンを起動し、動作確認を行えるようにする。なお、オプションでしないとローカルホストからしかアクセスできない。eginxでリバースプロキシアクセスしているのであれば問題ない。
SSG対象であってもSSRで動作し、ソースコードに変更があればブラウザのリロードですぐに反映される。
開発用にJSファイルのロードやHTML属性の挿入がされるので本番動作時とはHTMLは完全に一致しない。絶対パス情報等が挿入されるので、devモードで本番利用してはならない。
preview
SSGモードでのみ利用でき、ビルド済みの静的ファイルを配信するHTTPデーモンを起動する。ビルドはしないので、ユーザコードに変更があるのであればビルドしてから実行すること。
SSRモードでは使用できない。
check
ユーザコードに対して診断を行う。TypeScriptの型チェックも行うが、.astro拡張子のファイルしか対象としない。
build
ユーザコードをビルドし、SSG対象であれば静的ファイルを生成し、SSR対象であればプログラムを1ファイルに集約し、そのまま実行できるJavaScriptファイルを生成する。
設定ファイル
参考:https://docs.astro.build/ja/reference/configuration-reference/
astro.config.mjsファイルを変更することで、astroの動作について設定を行える。
開発環境の準備
開発環境は以下とする
- OS : Windows 10
- エディタ : VS Code
VS Codeのインストール
- VS Codeのサイトからダウンロードしてインストールする。
インストールパスなどのオプションは自身の好きな通り設定すればよい。 - インストール後にVS Codeを開き、左サイドバーメニューの「Extensions」を開き、「Japanese Language Pack for VS Code」で検索し、インストールする
- インストール後にポップアップで再起動するか聞かれるので再起動する。
Astro 拡張機能のインストール
- こちらよりインストールボタンをクリックする
- VS Codeが開かれ、拡張機能のインストールページが表示されるので、再度インストールボタンをクリックする
Node.jsのインストール
- バージョン一覧からサーバにバージョンを合わせて、バージョン16の最新版のインストーラーをダウンロードする
- 対象バージョンのReleasesを開く
- 末尾がx64.msiのものをダウンロードする
- インストーラーを実行する。オプションはデフォルトでよい。インストールパスについては変更したいのなら変更してもよい。
- PCを再起動する(PCを再起動しないとVS CodeでインストールしたNode.jsのパスが読み込まれない)
プロジェクト作成
- Windowsのエクスプローラーで任意の場所にフォルダを作る
- 作成したからのフォルダをVS Codeにドラッグアンドドロップする
- 作成者を信頼するか聞かれるので信頼するをクリックする
Astroのインストール
- VS Codeの上部メニューバーの「表示」から「ターミナル」を選択する
- ターミナルのペインが開いたら下記を実行する
※プロジェクトのフォルダに何かしらのファイルがあるとエラーになるので、その場合は一度全て消してから行う
npm create astro@latest -y
対話形式でインストールする。インストール場所については”.”と入力してプロジェクトのルートに設定する。その他はデフォルトでよい。 - インストール完了後、astroコマンドのパスを通す
- 左メニューバーの「管理」から「設定」を開く
- 設定の検索に「terminal.integrated.env.windows」と入力する
- ワークスペースタブを開く
- Terminal > Integrated > Env: Windowsの”setting.jsonで編集”をクリックする
- 下記を追記して保存する(Ctrl+S)
"terminal.integrated.env.windows": {
"PATH": "${workspaceRoot}\\node_modules\\.bin;${env:PATH}"
}※他に設定がなければ、下記のようになる。
{
"terminal.integrated.env.windows": {
"PATH": "${workspaceRoot}\\node_modules\\.bin;${env:PATH}"
}
}
- 上部メニューバーのターミナルから新しいターミナルを開く
- 調査用データをastroの運営元に送信しないようにする
※もしastroコマンドが見つからない(認識されません)というエラーが出た場合は、VS Codeを再起動してみる
astro.cmd telemetry disable
- 動作確認用にサーバとして起動する
astro.cmd dev
- ブラウザでhttp://127.0.0.1:3000/にアクセスし、Astroのページが表示されれば問題ない
- 終了する場合はCtrl+Cを入力する
SSRモードのインストール
SSRモードとして開発するのであれば次のコマンドを実行してモジュールをインストールする
astro.cmd add node -y
ビルドする。この時、Windowsファイアウォールでブロックされる可能性があり、その場合は管理者権限で許可が必要。
astro.cmd build
サーバを起動させ、ブラウザでアクセスして動作を確認する
node ./dist/server/entry.mjs
デバッグ
Astroをデバッグモードで起動することで、ブラウザで動作確認できる
ターミナル(上部メニューの ターミナル > 新しいターミナル)を開いて下記コマンドを実行する。
プログラムに更新があれば自動で反映される。
astro.cmd dev
デバッグモードの場合は本番時と多少出力されるHTMLが異なる。
本番時と同様のHTMLを出力されるようにするには下記コマンドを実行する。
astro.cmd build
node ./dist/server/entry.mjs
Tips
文法チェック
「問題」タブを確認することで、VS Codeが自動で行った型チェック等の問題を確認できる。
ただし、開いているファイルのみが対象となるので、ファイルを閉じると問題に表示されなくなる。
また、astro checkで検出されるよりも、細かな問題が検知される。
スクリプトの実行許可
Prisma等の独自コマンドを実行する場合、そのコマンドがpsファイルを(内部的に)使用している場合はデフォルトでは以下のようなエラーが出て実行が禁止されてしまう。
このシステムではスクリプトの実行が無効になっているため、ファイル ~~~\node_modules\.bin\prisma.ps1 を読み込むことができません。詳細については、「about_Execution_Policies」(https://go.
microsoft.com/fwlink/?LinkID=135170) を参照してください。
実行を許可するために、以下の設定をVS Codeに追加する
- settings.jsonを開く(左サイドバーにある)
- 以下を定義する
- terminal.integrated.env.windowsが既にある場合、その中に下記を追加する
"PSExecutionPolicyPreference": "RemoteSigned"
- terminal.integrated.env.windowsが定義されていない場合は、以下を追加する
"terminal.integrated.env.windows": {
"PSExecutionPolicyPreference": "RemoteSigned"
}
- terminal.integrated.env.windowsが既にある場合、その中に下記を追加する
- 保存後、VS Codeを再起動する
UIライブラリの使用
Astroでは<script>要素内も可能な範囲でサーバサイドで事前実行されるが、UIライブラリのようなクライアントサイドでインタラクションのあるUIを生成する場合はサーバサイドで実行しては正常に動作しない。
<script>要素にis:inline属性を付けることで強制的にすべてをクライアントサイドでの実行にできるが、<script>要素内ではフロントマター部分(astroファイルの—で囲まれた部分)で定義した変数の結果代入ができない。(文字列であれば一応やる方法はある。配列等のオブジェクトはできない)
参考:https://docs.astro.build/ja/guides/client-side-scripts/
そのため、UIライブラリが対応しているか次第ではあるが、HTMLとしてデータを配置し、そのHTMLを対象としてリッチUIに変換するJavaScriptコードをスクリプト部分に書く必要がある。
以下では表のリッチUIライブラリである、Grid.jsを使用した例である。
なお、Astroにおけるループ要素表示は配列のfor文ではなく、map関数で行う必要がある。
- 配列を使用したパターン
--- const data = [ ["John", "john@example.com", "(353) 01 222 3333"], ["Mark", "mark@gmail.com", "(01) 22 888 4444"], ["Eoin", "eoin@gmail.com", "0097 22 654 00033"], ["Sarah", "sarahcdd@gmail.com", "+322 876 1233"], ["Afshin", "afshin@mail.com", "(353) 22 87 8356"] ]; --- <link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet"/> <script is:inline src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script> <div id="wrapper"></div> <table id="table1"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Phone Number</th> </tr> </thead> <tbody> {data.map((Next) => ( <tr> <td>{Next[0]}</td> <td><a href={"mailto:"+Next[1]}>{Next[1]}</a></td> <td>{Next[2]}</td> </tr> ))} </tbody> </table> <script> new gridjs.Grid({ from: document.getElementById("table1") }).render(document.getElementById("wrapper")); </script>
- 連想配列を使用したパターン
--- const data = [ {name:"John", mail:"john@example.com", tel:"(353) 01 222 3333"}, {name:"Mark", mail:"mark@gmail.com", tel:"(01) 22 888 4444"}, {name:"Eoin", mail:"eoin@gmail.com", tel:"0097 22 654 00033"}, {name:"Sarah", mail:"sarahcdd@gmail.com", tel:"+322 876 1233"}, {name:"Afshin", mail:"afshin@mail.com", tel:"(353) 22 87 8356"} ]; --- <link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet"/> <script is:inline src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script> <div id="wrapper"></div> <table id="table1"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Phone Number</th> </tr> </thead> <tbody> {data.map((Next) => ( <tr> <td>{Next.name}</td> <td><a href={"mailto:"+Next.mail}>{Next.mail}</a></td> <td>{Next.tel}</td> </tr> ))} </tbody> </table> <script> new gridjs.Grid({ from: document.getElementById("table1") }).render(document.getElementById("wrapper")); </script>