ブログトップ

公開日:

更新日:

15 min read

技術革新

Astroサイトの記事を自動公開する - Cloudflare PagesとGitHub Actions連携ガイド

Astroサイトの記事を自動公開する - Cloudflare PagesとGitHub Actions連携ガイドのイメージ

このウェブサイトは、astroで構築しています。いわゆる「海」だけの静的サイトで、 ReactやVue.jsのような「島」つまりアイランドは使っていません。 いずれ使ってみたいとは思います。 これをcloudflare pagesでホスティングしています。

さらば、WordPress

それまではWordPressで100件以上ものシステムを開発・運用してきましたが、使い込めば使い込むほど、WordPressの嫌な面が目立つようになりました。 特にプラグインとバージョンアップです。 プラグインはセキュリティリスクになることが多いために、頻繁にバージョンアップしますが、それが原因でシステムが動かなくなることが珍しくありません。 本体のバージョンアップでも同じです。 しかも、過去に様々な脆弱性があったために、今でもそれを攻撃してくる輩が多く、対処するためにもバージョンアップは欠かせません。

疲れるんです。重いんです。面倒くさいんです。

外注に制作を依頼したホームページ(WordPress)10件が、納期数日前に攻撃されてすべて消えた、なんて漫画みたいなことも実際にありました。 このとき、同じ業者に委託していた、納期3週間前のECサイトも3つほど消えたのですが、こちらは、そもそも開発すらしていなかったのを、サイバー攻撃のせいにされた気配が濃厚でした。 ホームページは何とか復帰しましたが、その後、業者とは連絡が取れなくなりました。

ここからが修羅場です…ああ、思い出したくない。 だって、納品されたシステムのURLをたたいたら、WordPressのウェルカムページが表示されたんです… そういえば…ああ、もういいや。 WordPressはつらい思い出ばかりだ。

姑息なphpも好きじゃない

加えて、私はphpが好きではない。php3の頃から使っていますが、あのいい加減な仕様が嫌いです。 クラスに動的にメンバーを追加できるとか、便利さよりもバグの温床のリスクの方がはるかに多いです。 クラスにメンバー追加したきゃ、隠れてこそこそやるんじゃなくて、堂々と継承して拡張して使いやがれ!とジジイは思ってしまうのです。 その点、C++の方が王道を行ってると感じてしまう。

こんにちわ、astro

攻撃されなきゃいいんだろ? 動的サイトだから面倒なんだろ? このような短絡的な結論に至った私は、以前から興味があった静的ジェネレーターを使ってみたいと思いました。 いくつか試してみました。

  • Hugo : よく分からないし、Go言語も知らない。大規模サイト用と書いてある。Go言語はあまりそそられない。→却下
  • Jekyll : Rubyって使ったこともないのに、なんだか好きになれなくて、それでもがんばってみた。Jekyllは使いにくかった。→却下
  • astro : 分かりやすいし、node.jsとtypescriptは前からやってみたかった。→採用

さらに以前から気になっていた「サーバーレス」を試してみたかったので、cloudflare pagesを使うことにしました。 そうしたら、なんということでしょう! astroで記事を書いて、GitHubリポジトリにpushすると、Cloudflareが自動デプロイしてくれるのですごく便利です。 WordPressでの苦労が嘘みたいです。静的ページだから、攻撃されるリスクもないし、デプロイも楽だし、サーバーレスだからコストもかからないし、速いし、SEOもいいし、いいことづくめです。 おまけに無料!!!

爺は満足ぢゃ!

そうやって、がつがつと記事を書いているうちに、公開の手間が面倒臭くなってきました。 WordPressはCMSなので、公開日時を指定しておけば、指定した日時に自動公開してくれます。 ですがastroは静的サイトジェネレーターなので、そういう機能はありません。

ないんです。

こんな便利なもの、世界中で使われているなら、自動公開のニーズも高いはずです。 調べてみたら、同じことをしている人がいたのですが、記事がいまいちわかりにくかったので、整理したものをここに記録しました。

自動公開の仕組み

astroで記事を書いて、GitHubリポジトリにpushすると、Cloudflareが自動デプロイしてくれる機能を利用します。 Cloudflare pagesのデプロイフックなるものを利用します。 それをGitHub Actionsを使って自動デプロイする仕組みです。

PlantUML diagram

astro側の設定

  • フロントマターに公開日時を追加します。
  • フロントマターに従って、記事の公開を制御する機能を追加します

フロントマターの設定

私の場合、すでにフロントマターに公開日時を設定しているので、それを利用します。 ない場合は追加するといいでしょう。

   ---
title: 'システム開発の実際(中編):コーディングとプログラミング'
description: '1990年代初頭のシステム開発現場におけるプログラマーとSEの違い、開発環境の実態を記録。'
pubDate: '2024-12-18T00:00:00.000Z'
updatedDate: '2024-12-18T00:00:00.000Z'
heroImage: '/src/assets/images/blog/tech015-hero.webp'
headline: 'コーディングとプログラミング、PGとSEの違い - 1990年代初頭のシステム開発現場から'
category: '技術文化史'
tags: ['システム開発', 'COBOL', 'UNIX', 'パンチカード', '技術史']
draft: false
---

フロントマターの情報はsrc/content/config.tsに記述してあります。

   import { defineCollection, z } from 'astro:content';
import { CATEGORIES } from '@/data/categories';

const blog = defineCollection({
  // Type-check frontmatter using a schema
  schema: ({ image }) =>
    z.object({
      title: z.string().max(80),
      description: z.string(),
      // Transform string to Date object
      pubDate: z
        .string()
        .or(z.date())
        .transform((val) => new Date(val)),
      updatedDate: z
        .string()
        .or(z.date())
        .transform((val) => new Date(val)),
      heroImage: image(),
      category: z.enum(CATEGORIES),
      tags: z.array(z.string()),
      draft: z.boolean().default(false),
      keywords: z.string().optional(), // 追加
      headline: z.string().optional(),
      series: z.string().optional(),
      seriesIndex: z.number().optional(),
    }),
});

export const collections = { blog };

pubDatedraftを利用します。

公開処理の追加

utils/index.tsにgetAllPosts()関数を追加して、日付制御を行います。 さらに、元々あった下記の関数をオーバーライドして、getAllPosts()を利用するように変更しました。

  • getPosts()
  • getCategories()
  • getTags()
  • getPostByTag()
  • filterPostsByCategory()
   // src/utils/index.ts
import { getCollection } from 'astro:content';
export { sluglify, unsluglify } from './sluglify';
export { cn } from './cn';
export { remarkReadingTime } from './readTime';

// 記事の取得と公開制御の関数
export const getAllPosts = async () => {
  const posts = await getCollection('blog', ({ data }) => {
    const isNotDraft = import.meta.env.PROD ? data.draft !== true : true;
    const isNotFuture = import.meta.env.PROD
      ? data.pubDate.valueOf() <= new Date().valueOf()
      : true;
    return isNotDraft && isNotFuture;
  });

  // 日付でソート
  return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
};

// 既存の関数を修正
export const getPosts = async (max?: number) => {
  const posts = await getAllPosts();
  return max ? posts.slice(0, max) : posts;
};

export const getCategories = async () => {
  const posts = await getAllPosts();
  const categories = new Set(posts.map((post) => post.data.category));
  return Array.from(categories);
};

export const getTags = async () => {
  const posts = await getAllPosts();
  const tags = new Set(posts.flatMap((post) => post.data.tags));
  return Array.from(tags);
};

export const getPostByTag = async (tag: string) => {
  const posts = await getAllPosts();
  return posts.filter((post) => post.data.tags.includes(tag));
};

export const filterPostsByCategory = async (category: string) => {
  const posts = await getAllPosts();
  return category.toLowerCase() === 'all'
    ? posts
    : posts.filter((post) => post.data.category.toLowerCase() === category.toLowerCase());
};

このサイトではgetCollection()blogを指定しています。 これはsrc/content/config.tsの最後の行でblogと定義しているものです。 もしもここが

   export const collections = { post };

だったら、getAllPosts()のgetCollection()の引数はpostになります。

   // 記事の取得と公開制御の関数
export const getAllPosts = async () => {
    const posts = await getCollection('post', ({ data }) => {

cloudflare pagesの設定

1. デプロイフックの作成

  1. Cloudflareの管理画面で「Workers & Pages」を選択
  2. 「設定」タブを開く
  3. ページ下部の「デプロイフック」セクションを探す
  4. デプロイフックの右側の+をクリック

2. フック情報の入力

  • デプロイフック名:daily-deployなど分かりやすい名前
  • ブランチ:main(または使用しているブランチ名)
  • 「保存」ボタンをクリック

デプロイフックの作成

3. Webhook URLの取得

  • 作成後、デプロイフックのURLが表示される
  • このURLをコピーして保管

Webhook URLの取得

注意点

  • デプロイフックURLは公開しないように注意が必要です
  • テスト用のcurlコマンドも提供されるので、動作確認に使用できます:シェルで実行すれば結果が表示されます。

GitHub Actionsの設定

  • このURLをCLOUDFLARE_DEPLOY_WEBHOOKとして、GitHubのシークレットに設定する

GitHub Actionsでデプロイフックを設定する手順を説明します。

GitHubシークレットの設定手順

1. GitHubリポジトリの設定画面へアクセス

  • リポジトリのページを開く
  • 「Settings」タブを選択
  • 左メニューから「Secrets and variables」→「Actions」を選択

Webhook URLの取得

2. シークレットの追加

  • 「New repository secret」をクリック
  • 名前:CLOUDFLARE_DEPLOY_WEBHOOK
  • 値:Cloudflareから提供されたデプロイフックURL
  • 「Add secret」をクリック

Webhook URLの取得

注意点

  • シークレットの値は一度保存すると表示されません
  • シークレットの値を更新する場合は、再度全体を入力する必要があります
  • シークレットはリポジトリの管理者権限が必要です

これにより、安全にデプロイフックURLを管理しながら、自動デプロイを実行できます。

GitHub Actionsワークフローの設定

GitHub Actionsのワークフローを設定する手順を説明します。

基本的な設定手順

1. ディレクトリとファイルの作成

  • リポジトリのルートに.github/workflowsディレクトリを作成
  • ワークフローファイルdaily-deploy.yamlを作成

2. ワークフローファイルの作成

   name: 'Daily Deploy'

on:
  schedule:
    - cron: '0 0 * * *' # 毎日UTC 00:00に実行
  workflow_dispatch: # 手動実行用

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: curl -X POST -d {} ${{ secrets.CLOUDFLARE_DEPLOY_WEBHOOK }}

3. ワークフローの実行許可

  • リポジトリの Settings → Actions → General で Actions が有効にする
  • プライベートリポジトリなので、“Allow aqz-saito actions and reusable workflows”を選択

こうすることで、.github/workflowsディレクトリ内のyamlファイルが自動的に実行されます。

今後、actions/checkout@v4 など、信頼できる公式アクションを使用する必要がある場合は、 “Allow aqz-saito, and select non-aqz-saito, actions and reusable workflows”に変更します。

注意点

  • ファイルは必ず.github/workflowsディレクトリに配置
  • ファイル拡張子は.ymlまたは.yamlを使用
  • インデントは正確に設定する必要がある
  • シークレット情報は必ずGitHubのシークレット機能を使用

これらの手順に従うことで、自動デプロイのワークフローを設定できます。

エラーログ

GitHub Actionsのワークフローのエラーログは以下の場所で確認できます:

  1. GitHub上での確認方法:
  • リポジトリのトップページ → 「Actions」タブをクリック
  • 実行されたワークフローの一覧が表示される
  • エラーのあるワークフローは❌マークで表示
  • ワークフローをクリックすると詳細なログが表示
  • 各ステップを展開して詳細なエラーメッセージを確認可能
  1. 具体的なパス:
   https://github.com/[アカウント名]/[リポジトリ名]/actions
  1. ログの確認ポイント:
  • エラーが発生したステップが赤く表示される
  • エラーメッセージと発生時のコンテキスト
  • 実行環境の情報
  • 各コマンドの実行結果
  • 環境変数(機密情報は非表示)
  1. 通知設定:
  • リポジトリの「Settings」→「Notifications」
  • ワークフローの失敗時にメール通知を受け取れる
  • Slackなどの外部サービスとの連携も可能
  1. デバッグ用のTIPS:
   # デバッグ情報を出力するステップの例
steps:
  - name: Debug
    if: failure() # エラー時のみ実行
    run: |
      pwd
      ls -la
      env

エラー発生時は、ログを確認して問題の箇所を特定し、必要な修正を行います。