Open4

Astro + Movable Typeでの共通パーツのビルド周りメモ

redamoonredamoon

Astro を利用することでサイト制作を行うことができます。
部分的にCMSを使ってる場合だとAPIを利用してビルドする形が多いです。

しかし、Movable Typeといった以前から静的配信しているCMSではDataAPIを利用してAstroに置き換えるとかえって面倒だったりクライアントワークでは旨味がありません。

静的なページはAstroで行いCMSの部分は従来どおりMTMLで行う形で考えました。

  • 静的ページ(ウェブページといったHTMLで管理しているページ) -> Astroで管理
  • CMSで管理しているページは従来どおりMTMLで行う
  • その他部分的にCMS化している部分はMTMLでJSONにしてブラウザでよみこませる

上記の仕様で考えてみた。

ワークフロー

componentといったパーツ群をpages配下でビルドする流れになるのですが、共通パーツといったヘッダーやフッターなどはMTでも利用しなければなりません。
そのため、pages配下にMTML用のHTML(ヘッダーやフッター等)を用意して、CIのビルド時にrysncやFTP でMTが配置されてるサーバへ配信するという形を取った。

Astroで生成されたファイルは、MTのテンプレートでファイルリンクする形をとれば管理画面を編集せずに変更した部分で再構築反映するようなことができると考えた。
流れは以下のような形になる。

  1. Astroでpages配下のtmpでMTMLを記述する
  2. ビルド結果を元にMTが配置されているサーバへ配信
  3. MT側の管理画面では配信された箇所でファイルリンクする
  4. 変更されたあとはMTで全体再構築を行う(ここだけは手動…)

Vercelといったホスティングではなくただのレンタルサーバだとこの構成になる。

Astroの問題点

MTMLで以下のような形でヘッダーだったりHeadの情報をテンプレートモジュールにしておくことがあります。

<!DOCTYPE html>
<html lang="ja">
  <head>
<title><mt:Var name='_name' /> | XXXX</title>
<meta name="description" content="XXXXX">
~~~~~~~~
    <link rel="stylesheet" href="/assets/index.css">
<script type="module" src="/assets/hoisted2.js"></script></head>
  <body class="<mt:Var name='_page' />">

これらをAstroで出力できないかと思って以下のように配置した。
components/BaseHead.astro は、静的のページでも利用するため共通のHeadを用意しました。

---
const { siteName, title, description, path, type } = Astro.props;
const url = path ? `https://XXXXX.co.jp${path}` : 'https://XXXXX.co.jp/'
const ogp = 'https://XXXXX.co.jp/common/img/ogimage.png'
const siteTitle = title ? `${title} | ${siteName}` : siteName
---
<title set:html={siteTitle} />
<meta charset="UTF-8">
<script src="../scripts/app.ts"></script>

components単体でビルドできない都合上、pagesでパーツをビルドさせています。
pages/tmp/Head.astro というMTで利用するテンプレートモジュール用のファイルを用意しました。

---
// tmp/Head.astro
import BaseHead from '../../components/BaseHead.astro';
---
<html lang="ja">
  <head>
<BaseHead siteName="XXXXX" title={`<mt:Var name='_name' />`} description="XXXXX" type="article" path="<mt:Var name='_path' />" />
    <link rel="stylesheet" href="/assets/index.css" />
</head>
  <body class="<mt:Var name='_page' />">

このテンプレートをつくる際に2つ問題が起きました

1. Doctypeが自動で出力される

pagesで出力されるため、Doctypeが自動で出力されます。

例えば以下のように pages/Button のようなパーツを出力した場合以下のようになります。

<button>ボタン</button>

出力結果にDOCTYPEが挿入される

<!DOCTYPE html>  // ここに挿入される
<button>ボタン</button>

この解決方法として、AstroのIntegration を利用してビルド時に削除してもらうようにしました。

astro.config.mjsにhooksを使うことで解決できました。
target !== 'dist/tmp/Head/index.html' と書いてるのは、HeadだけはDoctypeが必要のために書いています。

import { defineConfig } from 'astro/config';
import { writeFileSync, readFileSync } from 'fs';

export function removeDoctype(html) {
  return html.replace('<!DOCTYPE html>', '')
}
export default defineConfig({
  integrations: [
    {
      name: 'removeDoctype',
      hooks: {
        'astro:build:done': (options) => {
          const paths = options.routes.filter(({pathname}) => pathname.startsWith('/tmp/')).map(v => `dist${v.pathname}/index.html`);

          try {
            paths.map((target) => {
              return target !== 'dist/tmp/Head/index.html' && writeFileSync(
                target,
                removeDoctype(readFileSync(target, 'utf8')),
                'utf8'
              )
            })
          } catch (error) {
            console.log(error);
          }
        }
      }
    }
  ]
});

1. 閉じタグがなかったら、ビルド時の成果物は閉じタグが自動で付与される

ありがたい機能ではあるのですが、タグの閉じ忘れがあった場合はビルド時に閉じてくれます。

<body>

bodyタグだけだと結果は以下のようになります。

<body></body>
ここにcomponentとかのコードが入る

今回のMTMLのパーツでは <body class="<mt:Var name='_page' />"> で終わるため、上記のように補完されてしまうと問題がありました。

こちらも同様にIntegrationを利用することで解決させました。

import { defineConfig } from 'astro/config';
import { writeFileSync, readFileSync } from 'fs';

export function removeBody(html) {
  return html.replace('</body>', '')
}

// https://astro.build/config
export default defineConfig({
  integrations: [
    {
      name: 'removeDoctype',
      hooks: {
        'astro:build:done': (options) => {
          const paths = options.routes.filter(({pathname}) => pathname.startsWith('/tmp/')).map(v => `dist${v.pathname}/index.html`);

          try {
            paths.map((target) => {
              return target === 'dist/tmp/Head/index.html' && writeFileSync(
                target,
                removeBody(readFileSync(target, 'utf8')),
                'utf8'
              )
            })
          } catch (error) {
            console.log(error);
          }
        }
      }
    }
  ]
});

redamoonredamoon

上の構成図で進めて、Github ActionsでastroをBuildしてrsyncできたのでメモ
ざっと調べた感じ、rsyncで時間を食ってしまった。

6年近く使ってるがpem鍵だったので、そのままだと使えず(そもそもパスフレーズすらつけたことも忘れてた)OpenSSL 形式にパスフレーズなしで変換する必要があった。

openssl rsa -in PEM.pem -out key.key といった形でPEM.pemを key.keyに変換してた。
あとは必要に応じてGithubのsettingsからSecretsを設定すれば動作できた。

run: rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p PORT' -av --delete dist/ ${SSH_USER}@${SSH_HOST}:${DST_PATH}

POSTはsshのポートをいれます。

全体のymlはこのような感じに仕上がりました。
LinterとかそのあたりはあとでもできるのでBuildだけできました。

name: Astro Build CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
      - run: npm ci
      - run: npm run build --if-present
      - name: Generate ssh key
        run: echo "$SSH_PRIVATE_KEY" > key && chmod 600 key
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: Deploy with rsync over SSH
        run: rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p PORT' -av --delete dist/ ${SSH_USER}@${SSH_HOST}:${DST_PATH}
        env:
          DST_PATH: ${{ secrets.DST_PATH }}
          SSH_PORT: ${{ secrets.SSH_PORT }}
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
redamoonredamoon

Astroとは関係ないが、Movable Typeのアップデートを自動化するためGitHub Actionsを書いた。
これでアップデートとか手動でやらなくて済む。
Secretに予めDB情報などは必要な情報入力しておくこと

ハマったこと

  • rsync でやらしてしまったので、きちんと送信前にdry-runで確認すること(盛大にぶっ壊してしまった)
  • sedで置換するときDB_PASSWORDで &などの記号が入るとうまく置換できないためSecretのほうに \\ をいれて予めエスケープしておく
name: Movable Type CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      MT_VERSION: MT7-R5402 #MTのzipファイル名
      MT_DATA_PATH: docker/mt-data # フォルダ構成は docker-mt-lamp https://github.com/redamoon/docker-mt-lamp
      MT_SETTINGS_PATH: mt-settings
      CGI_PATH: /cgi-bin/mt
      DOCUMENT_ROOT: /var/www/PATH
      HTML_PATH: /html
      DOMAIN: http://XXX.co.jp
      DB_NAME: ${{ secrets.DB_NAME }}
      DB_USER: ${{ secrets.DB_USER }}
      DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
      SSH_PORT: ${{ secrets.SSH_PORT }}
      SSH_HOST: ${{ secrets.SSH_HOST }}
      SSH_USER: ${{ secrets.SSH_USER }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Unzip File
        # MTのファイルを解凍して、mtディレクトリにリネーム
        # MTの設定ファイルをリネーム(mt-config-original.cgi to mt-config.cgi)
        # 設定ファイルの確認(log)
        run: |
         unzip -qq $MT_DATA_PATH/$MT_VERSION.zip -d $MT_DATA_PATH \
         && mv $MT_DATA_PATH/$MT_VERSION $MT_DATA_PATH/mt \
         && ls -la $MT_DATA_PATH/mt \
         && mv $MT_DATA_PATH/mt/mt-config.cgi-original $MT_DATA_PATH/mt/mt-config.cgi \
         && ls -la
      - name: MT Config CGI
        # MTの設定ファイルを編集
        # DBの設定を環境変数に変更(DBPASSは & が入ってるため SecretPASSに \\ を挿入している)
        # 言語設定を日本語に変更
        # StaticWebPathとCGIPathを環境変数に変更
        run: |
          cd ${{ env.MT_DATA_PATH }}/mt/ \
          && sed -i 's#StaticWebPath    http://www.example.com/mt-static#StaticWebPath    ${{ env.DOMAIN }}${{ env.CGI_PATH }}/mt-static#g' mt-config.cgi \
          && sed -i 's#CGIPath    http://www.example.com/cgi-bin/mt/#CGIPath    ${{ env.DOMAIN }}${{ env.CGI_PATH }}/#g' mt-config.cgi \
          && sed -i 's#Database DATABASE_NAME#Database ${{ env.DB_NAME }}#g' mt-config.cgi \
          && sed -i 's#DBUser DATABASE_USERNAME#DBUser ${{ env.DB_USER }}#g' mt-config.cgi \
          && sed -i 's#DBPassword DATABASE_PASSWORD#DBPassword '"${{ env.DB_PASSWORD }}"'#g' mt-config.cgi \
          && sed -i 's#DefaultLanguage en_US#DefaultLanguage ja#g' mt-config.cgi \
          && less mt-config.cgi
        shell: /usr/bin/bash -e {0}
      - name: Chmod
        # CGIに実行権限を加える
        run: chmod -R 755 $MT_DATA_PATH/mt/*.cgi && ls -la $MT_DATA_PATH/mt
        # rsyncにdeployするための秘密鍵の設定
      - name: Generate ssh key
        run: echo "$SSH_PRIVATE_KEY" > key && chmod 600 key
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: Deploy with rsync over SSH
        # rsyncでデプロイ(MT本体)
        run: rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_DATA_PATH}/mt/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}
      - name: MT Theme / Plugins Deploy with rsync over SSH
        # rsyncでデプロイ(テーマとプラグイン)
        # rsyncでuser-filesをデプロイ
        # rsyncでPluginのmt-staticをデプロイ
        # DataAPIProxyのPermission変更
        run: |
          rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete mtml/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/themes \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_SETTINGS_PATH}/plugins/DataAPIProxy/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/plugins/DataAPIProxy \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_SETTINGS_PATH}/plugins/MTAppjQuery/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/plugins/MTAppjQuery \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_SETTINGS_PATH}/plugins/PageBute/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/plugins/PageBute \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_SETTINGS_PATH}/plugins/PlainTextField/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/plugins/PlainTextField \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_SETTINGS_PATH}/plugins/ScheduleField/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/plugins/ScheduleField \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_SETTINGS_PATH}/plugins/UploadDir/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/plugins/UploadDir \
          && chmod -R 755 ${MT_SETTINGS_PATH}/plugins/DataAPIProxy/*.cgi \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete ${MT_SETTINGS_PATH}/mt-static/plugins/MTAppjQuery/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${CGI_PATH}/mt-static/plugins/MTAppjQuery \
          && rsync -e 'ssh -i key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p {{POST}}' -av --delete src/user-files/ ${SSH_USER}@${SSH_HOST}:${DOCUMENT_ROOT}${HTML_PATH}/user-files