Astro

Contents

Astroについて

AstroはJavaScript製のSSGツールであり、以下の機能を持つ

 

環境

AWS EC2上に構築する。

本番環境については付加状況等を考慮してスペックを決める必要がある。今回はテスト用の環境として以下を使用した。

 

構成

以下で構成する

 

TypeScriptは初期状態で使用できるので特別な構築作業は不要である。

 

Astroに限らずSSGは基本的にスタンドアローンのツールであり、自身のプログラムに組み込み、呼び出して使用するものではない。そのため、通常の使用ではAstroはNode.jsで実行する。
※SSGでもテスト用に内部でサーバ(HTTPデーモン)機能を保持していることがある。

ただし、今回はAstroはSSG兼SSRとして使用するので、SSR用にHTTPデーモンを動作させる。HTTPデーモンについては実行ランタイムが必要となり、標準のNode.jsを使用する。

 

サーバの構築

AWS上での設定

EC2でサーバ作成後、詳細は割愛するが以下設定が追加で必要

 

rootになる

各設定を始める前にrootユーザになっておく

sudo su -

 

firewalld

アクセスコントロールはAWSで行うので、サーバ上では設定しない

 

SELinux

標準で無効化されている

[root@ip-10-0-13-104 ~]# getenforce
Disabled

 

その他のLinuxの設定

SSHのポート番号の変更

デフォルトのSSHポート(TCP/22)をインターネットに公開していると不正アクセスをしようとするアクセスが多くなり、アクセスに失敗したとしても不要なログが増えがちである。

ポート番号を変更することである程度軽減できる。

この変更を行う場合、EC2のセキュリティグループの許可設定追加も必要となる。

  1. 現在はポート指定がないことを確認(デフォルトポートで動作している)
    grep "^#Port 22" /etc/ssh/sshd_config
    • ログ
      [root@ip-10-0-13-104 ~]# grep "^#Port 22" /etc/ssh/sshd_config
      #Port 22
  2. 設定変更(ここでは7542番ポートにしているが実際は何でもよい)
    sed -i -e "s/^#Port 22/Port 7542/g" /etc/ssh/sshd_config
  3. 設定が変更されたことを確認
    grep "^Port " /etc/ssh/sshd_config
    • ログ
      [root@ip-10-0-13-104 ~]# grep "^Port " /etc/ssh/sshd_config
      Port 7542
  4. sshdを再起動する
    systemctl restart sshd
  5. 再起動後、待ち受けポート番号が変わっていることを確認
    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では専用リポジトリからインストールする。

  1. nginxをインストールする
    amazon-linux-extras install nginx1 -y
  2. サービスを起動する
    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.
  3. ブラウザでHTTPアクセスできることを確認する
    Welcome to nginx! ページが表示されれば成功
    http://<ドメイン名>/
  4. nginxの設定ファイルを編集し、使用するドメイン名を記載する。
    この変更を行わないとLet’s Encryptの証明書インストール時にエラーが出る。
    nginxの再起動は不要。
    vi /etc/nginx/nginx.conf
    以下を変更する(コメントアウトされていない箇所のみ)
    server_name _;
    変更後
    server_name <ドメイン名>;
  5. epelリポジトリをインストールする
    amazon-linux-extras install epel -y
  6. Let’s Encrypt用ツールをインストールする
    yum install certbot python-certbot-nginx -y
  7. 証明書を取得・インストールする。
    この時登録したメールアドレス宛にニュース等を送ってよいか聞かれるので、不要であればNを入力する。
    certbot run \
    --nginx \
    --agree-tos \
    --email <メールアドレス> \
    --domain <ドメイン名>
  8. ブラウザでHTTPSアクセスできることを確認する
    Welcome to nginx! ページが表示されれば成功

    https://<ドメイン名>/
  9. nginxの設定ファイルを編集し、リバースプロキシ設定を記載する。
    vi /etc/nginx/nginx.conf
    以下を追記する(HTTPS用のserverディレクティブ内、ssl_dhparamの後くらいに追記する)
        location / {
    proxy_pass http://127.0.0.1:3000/;
    }
  10. 設定ファイルの文法チェックを行う
    nginx -t
    結果
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
  11. nginxを再起動する
    systemctl restart nginx
    systemctl status nginx

 

 

astroのインストール

Node.js環境のインストール

  1. nvm(Node.js自体のバージョン管理プログラム)をインストールする
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
  2. ホームディレクトリからシステムディレクトリに移動する
    mv /root/.nvm /usr/local/nvm
  3. nvmからnpmをインストールする
    ※Amazon Linux 2はNPMバージョン16までしか対応していないのでバージョン指定すること
    cd /usr/local/nvm
    . nvm.sh
    nvm install 16
  4. インストールできたことを確認数
    node -e "console.log('Running Node.js ' + process.version)"
  5. 実行ファイルのエイリアスを作成する
    ln -s `which npm` /usr/local/nvm/alias/npm
    ln -s `which node` /usr/local/nvm/alias/node
  6. 実行パス用の環境変数をログイン時に設定するようにする
    echo "" >> /etc/bashrc
    echo "# NODE ENVIRONMENT VARIABLE" >> /etc/bashrc
    echo "export PATH=/usr/local/nvm/alias:\$PATH" >> /etc/bashrc
  7. 現在のログインにおいても環境変数を適用する
    export PATH=/usr/local/nvm/alias:$PATH

 

astroパッケージのインストール

以降ではastroのインストールと共にastroのプロジェクトを作成する。新規にプロジェクトを作成するたびに行う必要がある。

今回はプロジェクト名をastro-testとした。

  1. インストール用のディレクトリを作成する
    mkdir /usr/local/astro-test
    cd /usr/local/astro-test
  2. 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
  3. 実行ファイルのエイリアスを作成する
    ln -s `pwd`/node_modules/astro/astro.js /usr/local/nvm/alias/astro
  4. 調査用データをastroの運営元に送信しないようにする
    astro telemetry disable
  5. 開発用のHTTPデーモンを起動する
    astro dev
  6. ブラウザでHTTPSアクセスし、Welcome to Astroページが表示されることを確認する。

 

インストール完了後、srcディレクトリ内のユーザプログラム相当のサンプルをいろいろ変更することでブラウザでも変更を確認できる。

 

Node.jsアダプタのインストール

SSRモードで動作させる際にサーバープログラムを動作させるランタイムが必要になる。Node.jsであれば、Node.js自体はインストール不要であるが、astroとNode.jsを連携させるアダプタをインストールしなければならない。

参考:https://docs.astro.build/ja/guides/integrations-guide/node/

 

  1. アダプタをインストールする。
    -yオプションをつけない場合は対話形式でnodeのインストールで間違いがないか、設定ファイルに変更を加えても問題ないか問われるので、両方そのままエンターを押して進める。
    astro add node -y
  2. 設定ファイルを書き換える
    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
  3. ビルドできることを確認する
    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!
  4. サーバを起動させる
    node ./dist/server/entry.mjs
  5. ブラウザでアクセスできるか確認する

 

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等も同じである。

 

導入する場合は以下の手順を実施する。

 

  1. astro様にdenoプラグインをインストールする
    astro.js add deno
  2. 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サイト開発に必要な機能を組み込んだサンプルシステムを開発する。

以下の機能を組み込む

上記を踏まえて開発するのは下記である

 

前準備

 

 

MariaDBのインストール

MariaDBを使用するデータベースとしてローカルにインストールする。

  1. 使用可能なバージョンを確認する
    amazon-linux-extras | grep -i maria
  2. 最新バージョンインストールする
    ※yumでインストール可能であるが、yumでインストールするとバージョンが古く、mattermost使用時にFULLTEXTインデックスが使用できないとしてエラーになる。
    amazon-linux-extras install mariadb10.5 -y
  3. サービスを起動する
    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
  4. MariaDBに接続する
    mysql
  5. ユーザーを作成する。
    パスワードは任意のものに変更してよい。
    create user 'astrotest'@'localhost' identified by 'astrotest';
  6. データベースを作成する
    create database astrotest;
  7. 作成したユーザーにアクセス権限を付与する
    grant all privileges on astrotest.* to 'astrotest'@'localhost';
  8. ログアウトする
    exit
  9. こちらのサイトからログイン情報用パスワードを生成しておく。
    変換ルールを1:プレーンTEXT、2:変換不要、3:SHA-256、4:BASE64とする。
    例としてpass1、pass2という文字列の場合、それぞれ以下のようになる。
    pass1:5sPaWyBmNNfz81htdH/9s2tcZ1dXs4DGpf5cVwxxQ0k=
    pass2:G6PRbpiBlZ+MmpdihU9yxuYyHN1ENYoQpOk5AzEX6rk=
  10. 作成したユーザでログインできるか確認する
    mysql -u astrotest -pastrotest astrotest
  11. ログイン情報用テーブルを作成する
    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;
  12. ログイン情報用データを挿入する
    insert into User (UserId,Password) values ('usera','5sPaWyBmNNfz81htdH/9s2tcZ1dXs4DGpf5cVwxxQ0k=');
    insert into User (UserId,Password) values ('userb','G6PRbpiBlZ+MmpdihU9yxuYyHN1ENYoQpOk5AzEX6rk=');
  13. 挿入データを確認する
    select * from User;
  14. コンテンツ用テーブルを作成する
    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;
  15. コンテンツ用データを挿入する
    insert into Contents (Name,Value) values ('AAA','111'),('BBB','222');
  16. 挿入データを確認する
    select * from Contents;
  17. 編集用コンテンツ用テーブルを作成する
    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;
  18. 編集用コンテンツ用データを挿入する
    insert into Article (Title,Body,EditorId) values ('Test','Test',1);
  19. 挿入データを確認する
    select * from Article;
  20. ログアウトする
    exit

 

ORM(Prisma)のインストール

ORMとしてはPrismaを利用する。TypeScriptで使用できる主なORMとしてはSequelize、TypeORM、Prismaがある。
SQLのパフォーマンスチューニングやトラブルシューティングがしやすいのでクエリビルダーによる自動SQL生成ではなく、生のSQLを使用したい。ただし、クラス等を使用しての型付け・オブジェクト化をselect・insert・update全てでサポートしている必要がある。
※ORM(Object Relation Mapper)とはデータベースのデータとプログラミング言語上のオブジェクトを自動で対応させて扱えるようにするデータベース操作用ライブラリである。

 

  1. prismaをインストールする
    npm install prisma
  2. prismaの設定ファイル類を生成する
    ./node_modules/.bin/prisma init
  3. 設定ファイルを書き換える
    cat > prisma/schema.prisma << EOT
    generator client {
      provider = "prisma-client-js"
    }
    
    datasource db {
      provider = "mysql"
      url      = env("DATABASE_URL")
    }
    EOT
  4. 環境用の設定ファイルを編集する
    vi .env
    以下を末尾に張り付ける
    DATABASE_URL = 'mysql://astrotest:astrotest@localhost:3306/astrotest'
    その際# This was inserted by `prisma init`:以下部分は削除しておく
  5. DBに作成したテーブルから定義情報を抽出して設定に取り込む
    ./node_modules/.bin/prisma db pull
  6. 設定ファイルを確認する
    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?
    }
  7. 以下を末尾に追記する
    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)
    }
  8. 設定ファイルをビルドする
    ./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ファイル

別のファイルから呼び出して使用するユーティリティ関数。以下がある。

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配下は除外

 

サンプルプロジェクト込みでインストールした場合、astroインストール直後のファイルの構成は以下のようになる。
※node_modules配下は除外

 

各ディレクトリ・ファイルについて

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/#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

 

リクエストヘッダー・ボディの受け取り

リクエストヘッダー等のリクエスト時にクライアントから渡された情報を参照できる。

参考:https://docs.astro.build/ja/core-concepts/endpoints/#request

 

スクリプトによるレスポンスの生成

HTMLテンプレートによるのではなく、ビルド時に実行されたスクリプトの結果をレスポンスとして返すことができる。

参考: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

 

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の仕組みを使用するので、クライアントによりリダイレクト先へ再度のアクセスがされることになる(サーバ側で代理取得して返すわけではない)。

参考:https://docs.astro.build/ja/core-concepts/endpoints/#%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88

 

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

 

 

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の動作について設定を行える。

 

開発環境の準備

開発環境は以下とする

 

VS Codeのインストール

  1. VS Codeのサイトからダウンロードしてインストールする。
    インストールパスなどのオプションは自身の好きな通り設定すればよい。
  2. インストール後にVS Codeを開き、左サイドバーメニューの「Extensions」を開き、「Japanese Language Pack for VS Code」で検索し、インストールする
  3. インストール後にポップアップで再起動するか聞かれるので再起動する。

 

Astro 拡張機能のインストール

  1. こちらよりインストールボタンをクリックする
  2. VS Codeが開かれ、拡張機能のインストールページが表示されるので、再度インストールボタンをクリックする

 

Node.jsのインストール

  1. バージョン一覧からサーバにバージョンを合わせて、バージョン16の最新版のインストーラーをダウンロードする
    1. 対象バージョンのReleasesを開く
    2. 末尾がx64.msiのものをダウンロードする
  2. インストーラーを実行する。オプションはデフォルトでよい。インストールパスについては変更したいのなら変更してもよい。
  3. PCを再起動する(PCを再起動しないとVS CodeでインストールしたNode.jsのパスが読み込まれない)

 

プロジェクト作成

  1. Windowsのエクスプローラーで任意の場所にフォルダを作る
  2. 作成したからのフォルダをVS Codeにドラッグアンドドロップする
  3. 作成者を信頼するか聞かれるので信頼するをクリックする

 

Astroのインストール

  1. VS Codeの上部メニューバーの「表示」から「ターミナル」を選択する
  2. ターミナルのペインが開いたら下記を実行する
    ※プロジェクトのフォルダに何かしらのファイルがあるとエラーになるので、その場合は一度全て消してから行う
    npm create astro@latest -y
    対話形式でインストールする。インストール場所については”.”と入力してプロジェクトのルートに設定する。その他はデフォルトでよい。
  3. インストール完了後、astroコマンドのパスを通す
    1. 左メニューバーの「管理」から「設定」を開く
    2. 設定の検索に「terminal.integrated.env.windows」と入力する
    3. ワークスペースタブを開く
    4. Terminal > Integrated > Env: Windowsの”setting.jsonで編集”をクリックする
    5. 下記を追記して保存する(Ctrl+S)
          "terminal.integrated.env.windows": {
            "PATH": "${workspaceRoot}\\node_modules\\.bin;${env:PATH}"
          }

      ※他に設定がなければ、下記のようになる。

      {
        "terminal.integrated.env.windows": {
            "PATH": "${workspaceRoot}\\node_modules\\.bin;${env:PATH}"
        }
      }
  4. 上部メニューバーのターミナルから新しいターミナルを開く
  5. 調査用データをastroの運営元に送信しないようにする
    ※もしastroコマンドが見つからない(認識されません)というエラーが出た場合は、VS Codeを再起動してみる
    astro.cmd telemetry disable
  6. 動作確認用にサーバとして起動する
    astro.cmd dev
  7. ブラウザでhttp://127.0.0.1:3000/にアクセスし、Astroのページが表示されれば問題ない
  8. 終了する場合は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に追加する

  1. settings.jsonを開く(左サイドバーにある)
  2. 以下を定義する
    • terminal.integrated.env.windowsが既にある場合、その中に下記を追加する
              "PSExecutionPolicyPreference": "RemoteSigned"
    • terminal.integrated.env.windowsが定義されていない場合は、以下を追加する
          "terminal.integrated.env.windows": {
            "PSExecutionPolicyPreference": "RemoteSigned"
        }
  3. 保存後、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>

 


Notice: Trying to get property 'queue' of non-object in /usr/local/wordpress/wp-includes/script-loader.php on line 2876

Warning: Invalid argument supplied for foreach() in /usr/local/wordpress/wp-includes/script-loader.php on line 2876