10分で Astro サイトを GitHub Pages に構築するスターターキットを作ったよ

# はじめに

こんにちは。debiru です。

今日は GitHub Pages を使って Astro を用いた Web サイトをデプロイする手順を紹介します。

自動デプロイスクリプトを用意したので、以下の 1. 〜 6. のセクションの手順を実施するだけで Astro サイトが爆誕します。

なお、今回は BeautifulCode を実現します。BeautifulCode とは、出力される HTML, CSS, JS のソースコードの可読性を担保することを指します。出力ソースコードの圧縮などをせず、インデントの整った美しいソースコードを提供することを目指します。

# 1. GitHub にリポジトリを作成する

- https://github.com/new

![リポジトリ作成画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178936/df34a4bb-b306-c77b-3d28-724ee05c1c47.png)

- お好きなリポジトリ名を入力します
- リポジトリの公開範囲は Public にします
- Add a README file にチェックを入れます

そして Create Repository ボタンを押します。

# 2. GitHub Pages で使うドメイン名の DNS 設定を行う

例えば `https://astro.debiru.net` という URL でサイトを公開したい場合、`astro.debiru.net` のドメイン名に対する DNS 設定を行います。

- https://docs.github.com/ja/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site

このマニュアルを参考に、頂点ドメイン名（`debiru.net` など）の場合は A レコードを、サブドメイン（`astro.debiru.net` など）の場合は CNAME レコードを設定します。ただしサブドメインであっても、他のレコード（MX や TXT など）を設定している場合は、頂点ドメイン名と同様に A レコードを用いて設定してください。

# 3. GitHub Pages の設定を行う

作成したリポジトリの GitHub 上のページで、上部のグローバルメニューから `Settings` を選び、左サイドバーにある `Pages` を選びます。

![GitHub Pages 設定画面(1)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178936/28648de9-b48f-688a-fdf6-6ee0971bbf13.png)

- Source から `GitHub Actions` を選んでおきます

![GitHub Pages 設定画面(2)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178936/52be9895-3af0-48fb-a09d-36227ad957d5.png)

- Custom domain に、使いたいドメイン名を入力します
- Enforce HTTPS にチェックを入れます（チェックが入れられない場合は、ページをリロードしてみてください）

# 4. リポジトリを clone する

作成したリポジトリを手元の作業 PC 上に clone します。

`astro` というリポジトリ名の場合は次のような感じのコマンドになるはずです。

```sh
git clone git@github.com:YOURNAME/astro.git

# clone したリポジトリに移動する
cd astro
```

# 5. Node と npm のインストール

もし手元の PC 上で node がインストールされていなければ、node をインストールします。

```sh
node -v

npm -v
```

それぞれ、バージョン情報が表示されれば OK です。最新安定版（2025年1月時点で node 22.x, npm 11.x）を使っていればなお良いです。

node のインストール手順が分からない方は [@debiru_R](https://x.com/debiru_R) までお尋ねください。mise などのパッケージ管理ソフトを使うとよいでしょう。

https://mise.jdx.dev/getting-started.html

# 6. Astro 環境を自動で構築する

*注意：以下のように外部シェルスクリプトを実行する際は、スクリプトの中身が安全かどうかを確かめてから実行するようにしてください。*

スクリプトの中身は https://astro.debiru.net でも紹介しています。

```sh
curl -fsSLO https://astro.debiru.net/build/astro-starter-kit.sh
chmod +x astro-starter-kit.sh

# スクリプトの中身を確認する
cat astro-starter-kit.sh

# Astro Starter Kit をインストールする
# astro.debiru.net の部分は、実際に利用したいドメイン名を指定してください
# ./astro-starter-kit.sh astro.debiru.net
./astro-starter-kit.sh

git push
```

これでデプロイ処理が実行されます。デプロイには30秒ほどかかるかもしれません。

作成したリポジトリの GitHub 上のページにアクセスして、画面上部の最終コミットログにチェックマークが付いているかを確認してください。

チェックマークが付いていれば、指定したドメイン名の URL（https://astro.debiru.net のような URL）にアクセスすることで Astro のサイトが閲覧できるはずです。

内部処理について読まなくてもよいという方は「[#おわりに](#おわりに)」へお進みください。

次のセクションでは、このスターターキット自動スクリプトが内部的に何を行っているかについて説明します。

# Astro 環境を手動で構築する

以下は手動での Astro 環境構築手順ですが、これはスターターキット自動スクリプトで実行している処理内容です。

## Astro プロジェクトを作成する

```sh
npm create astro@latest project -- --template minimal --no-install --no-git
mv project/{*,.*} . && rm -r project
npm install
```

## git commit しておく（1回目）

```sh
git add .
git commit -m "npm create astro@latest"
```

## .gitignore を編集する

デフォルトでは `dist/` と書かれていますが、これを `/dist/` に変更します。

後々、public の中で dist ディレクトリを使いたいケースに対応できなくなるため、この変更を行っておきます。

```diff
diff --git a/.gitignore b/.gitignore
index 016b59e..a34f621 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
 # build output
-dist/
+/dist/
 
 # generated types
 .astro
```

## git commit しておく（2回目）

```sh
git add .
git commit -m "update .gitignore"
```

## .github/workflows/deploy.yml を作成する

```sh
mkdir -p .github/workflows
touch .github/workflows/deploy.yml
```

`.github/workflows/deploy.yml` を編集して、以下の内容にします。

もし、Git のブランチ名が `main` でなければ、5行目のブランチ名の部分は変更してください。

```yml:.github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [ main ]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout your repository using git
        uses: actions/checkout@v4
      - name: Install, build, and upload your site
        uses: withastro/action@v3
        with:
          node-version: 22
          package-manager: npm@latest

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
```

## git commit しておく（3回目）

```sh
git add .
git commit -m "add .github/workflows/deploy.yml"
```

## src ディレクトリを空にする

デフォルトで作成される src ファイルを削除しておきます。

```sh
rm -r src && mkdir src
```

## git commit しておく（4回目）

```sh
git add .
git commit -m "remove default src files"
```

## package.json を更新する

```diff
diff --git a/package.json b/package.json
index 65e3364..9962f52 100644
--- a/package.json
+++ b/package.json
@@ -4,11 +4,13 @@
   "version": "0.0.1",
   "scripts": {
     "dev": "astro dev",
-    "build": "astro build",
+    "build": "build/build.sh && astro build",
+    "watch": "build/watch.sh",
+    "stylelint": "build/stylelint.sh",
     "preview": "astro preview",
     "astro": "astro"
   },
   "dependencies": {
     "astro": "^5.1.10"
   }
-}
\ No newline at end of file
+}
```

```sh
npm install -D glob js-beautify sass stylelint stylelint-config-standard
```

## git commit しておく（5回目）

```sh
git add .
git commit -m "update package.json"
```

## スターターキットを用意する

- sass, stylelint の実行を可能にする
- css, js, scss, img のアセットファイルを用意する
- `src/config/myExtIntegration.mjs` で Astro のビルド結果を加工する
- `src/config/view.mjs` でページ共通の初期設定をする
- `src/pages` でサンプルページを用意する

```
	new file:   .stylelintrc.json
	modified:   astro.config.mjs
	new file:   build/build.sh
	new file:   build/sass-compile.sh
	new file:   build/sass-watch.sh
	new file:   build/stylelint.sh
	new file:   build/watch.sh
	new file:   public/assets/css/global/reset.css
	new file:   public/assets/css/global/style.css
	new file:   public/assets/css/subpage/style.css
	new file:   public/assets/img/global/logo.png
	new file:   public/assets/img/global/og.png
	new file:   public/assets/js/global/base.js
	new file:   public/assets/js/subpage/script.js
	new file:   public/assets/scss/global/reset.scss
	new file:   public/assets/scss/global/style.scss
	new file:   public/assets/scss/subpage/style.scss
	new file:   public/favicon.ico
	new file:   src/config/Util.mjs
	new file:   src/config/myExtIntegration.mjs
	new file:   src/config/view.mjs
	new file:   src/layouts/Layout.astro
	new file:   src/pages/index.astro
	new file:   src/pages/subpage/index.astro
```

### スターターキットの差分

ファイルの内容を示すために、上記ファイル群の `git diff` の結果を貼っておきます。

- `astro.config.mjs` の `site: 'https://astro.debiru.net'` 部分は、ご自身の GitHub Pages に設定したい URL を指定してください

```diff
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 0000000..3832116
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,18 @@
+{
+    "extends": "stylelint-config-standard",
+    "rules": {
+        "alpha-value-notation": null,
+        "at-rule-empty-line-before": null,
+        "custom-property-pattern": null,
+        "declaration-block-no-redundant-longhand-properties": null,
+        "no-descending-specificity": null,
+        "no-duplicate-selectors": null,
+        "no-empty-source": null,
+        "property-no-vendor-prefix": null,
+        "selector-class-pattern": null,
+        "selector-id-pattern": null,
+        "rule-empty-line-before": null,
+        "selector-attribute-quotes": null,
+        "value-keyword-case": null
+    }
+}
diff --git a/astro.config.mjs b/astro.config.mjs
index e762ba5..f333444 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -1,5 +1,13 @@
-// @ts-check
 import { defineConfig } from 'astro/config';
+import myExtIntegration from '/src/config/myExtIntegration';
 
-// https://astro.build/config
-export default defineConfig({});
+// refs. https://astro.build/config
+export default defineConfig({
+  site: 'https://astro.debiru.net',
+  trailingSlash: 'ignore',
+  compressHTML: false,
+  integrations: [myExtIntegration()],
+  build: {
+    format: 'file',
+  },
+});
diff --git a/build/build.sh b/build/build.sh
new file mode 100755
index 0000000..7a7be25
--- /dev/null
+++ b/build/build.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+cd $(dirname $0)
+
+./sass-compile.sh
+./stylelint.sh
diff --git a/build/sass-compile.sh b/build/sass-compile.sh
new file mode 100755
index 0000000..dc59db8
--- /dev/null
+++ b/build/sass-compile.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+cd $(dirname $0)/..
+
+CSS_DIR="public/assets/css"
+SCSS_DIR="public/assets/scss"
+
+rm -r "${CSS_DIR}"
+npx sass --no-source-map "${SCSS_DIR}:${CSS_DIR}"
diff --git a/build/sass-watch.sh b/build/sass-watch.sh
new file mode 100755
index 0000000..194d364
--- /dev/null
+++ b/build/sass-watch.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+cd $(dirname $0)/..
+
+CSS_DIR="public/assets/css"
+SCSS_DIR="public/assets/scss"
+
+npx sass --watch --no-source-map "${SCSS_DIR}:${CSS_DIR}"
diff --git a/build/stylelint.sh b/build/stylelint.sh
new file mode 100755
index 0000000..2764123
--- /dev/null
+++ b/build/stylelint.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+cd $(dirname $0)/..
+
+npx stylelint "public/assets/css/**/*.css"
diff --git a/build/watch.sh b/build/watch.sh
new file mode 100755
index 0000000..3423ea6
--- /dev/null
+++ b/build/watch.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+cd $(dirname $0)
+
+./build.sh
+./sass-watch.sh
diff --git a/public/assets/css/global/reset.css b/public/assets/css/global/reset.css
new file mode 100644
index 0000000..bca856a
--- /dev/null
+++ b/public/assets/css/global/reset.css
@@ -0,0 +1,98 @@
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font: inherit;
+  font-size: 100%;
+  vertical-align: baseline;
+}
+
+html {
+  line-height: 1;
+}
+
+ol, ul {
+  list-style: none;
+}
+
+table {
+  border-collapse: separate;
+  border-spacing: 0;
+}
+
+caption, th, td {
+  text-align: left;
+  font-weight: normal;
+  vertical-align: middle;
+}
+
+q, blockquote {
+  quotes: none;
+}
+
+q::before, q::after, blockquote::before, blockquote::after {
+  content: "";
+  content: none;
+}
+
+a img {
+  border: none;
+}
+
+article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
+  display: block;
+}
+
+sup, sub {
+  font-size: 75%;
+}
+
+sup {
+  vertical-align: super;
+}
+
+sub {
+  vertical-align: sub;
+}
+
+textarea, pre {
+  font-family: monospace;
+}
+
+b, strong {
+  font-weight: bold;
+}
+
+i, em {
+  font-style: italic;
+}
+
+hr {
+  margin: 0;
+  padding: 0;
+  border: 0;
+}
+
+select {
+  font-size: 100%;
+}
+
+img, iframe, video {
+  vertical-align: top;
+}
+
+button {
+  cursor: pointer;
+}
diff --git a/public/assets/css/global/style.css b/public/assets/css/global/style.css
new file mode 100644
index 0000000..2babb3d
--- /dev/null
+++ b/public/assets/css/global/style.css
@@ -0,0 +1,64 @@
+html {
+  scroll-behavior: smooth;
+  font-size: 62.5%;
+}
+
+body {
+  -webkit-text-size-adjust: 100%;
+  font-size: 1.6rem;
+}
+
+.wbr {
+  display: inline-block;
+}
+
+.content-inner {
+  max-width: 1080px;
+  margin-inline: auto;
+  padding-inline: 10px;
+}
+
+#page-container {
+  display: grid;
+  grid-template-rows: auto 1fr auto;
+  grid-template-columns: 100%;
+  min-width: 100dvw;
+  min-height: 100dvh;
+}
+
+#page-header .content-inner {
+  border-bottom: 1px solid #ccc;
+}
+#page-header .siteName {
+  padding-block: 8px;
+}
+
+#page-main {
+  text-align: center;
+}
+#page-main .content-inner > * {
+  margin-top: 30px;
+}
+#page-main h1 {
+  font-family: serif;
+  font-weight: bold;
+  font-size: 3.2rem;
+}
+
+#page-footer {
+  border-top: 1px solid #999;
+  background: #f0f0f0;
+}
+#page-footer .footerArea {
+  padding: 4px 8px 8px;
+}
+#page-footer .footerArea .copyright {
+  padding-top: 4px;
+  text-align: center;
+  font-family: Verdana, sans-serif;
+  font-size: 1.4rem;
+}
+#page-footer .footerArea .copyright img {
+  margin-top: -4px;
+  vertical-align: middle;
+}
diff --git a/public/assets/css/subpage/style.css b/public/assets/css/subpage/style.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/public/assets/css/subpage/style.css
@@ -0,0 +1 @@
+
diff --git a/public/assets/img/global/logo.png b/public/assets/img/global/logo.png
new file mode 100644
index 0000000..b6bbfcf
Binary files /dev/null and b/public/assets/img/global/logo.png differ
diff --git a/public/assets/img/global/og.png b/public/assets/img/global/og.png
new file mode 100644
index 0000000..ca5f53b
Binary files /dev/null and b/public/assets/img/global/og.png differ
diff --git a/public/assets/js/global/base.js b/public/assets/js/global/base.js
new file mode 100644
index 0000000..bedd521
--- /dev/null
+++ b/public/assets/js/global/base.js
@@ -0,0 +1,3 @@
+(function() {
+  'use strict';
+})();
diff --git a/public/assets/js/subpage/script.js b/public/assets/js/subpage/script.js
new file mode 100644
index 0000000..bedd521
--- /dev/null
+++ b/public/assets/js/subpage/script.js
@@ -0,0 +1,3 @@
+(function() {
+  'use strict';
+})();
diff --git a/public/assets/scss/global/reset.scss b/public/assets/scss/global/reset.scss
new file mode 100644
index 0000000..bca856a
--- /dev/null
+++ b/public/assets/scss/global/reset.scss
@@ -0,0 +1,98 @@
+html, body, div, span, applet, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+a, abbr, acronym, address, big, cite, code,
+del, dfn, em, img, ins, kbd, q, s, samp,
+small, strike, strong, sub, sup, tt, var,
+b, u, i, center,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, embed,
+figure, figcaption, footer, header, hgroup,
+menu, nav, output, ruby, section, summary,
+time, mark, audio, video {
+  margin: 0;
+  padding: 0;
+  border: 0;
+  font: inherit;
+  font-size: 100%;
+  vertical-align: baseline;
+}
+
+html {
+  line-height: 1;
+}
+
+ol, ul {
+  list-style: none;
+}
+
+table {
+  border-collapse: separate;
+  border-spacing: 0;
+}
+
+caption, th, td {
+  text-align: left;
+  font-weight: normal;
+  vertical-align: middle;
+}
+
+q, blockquote {
+  quotes: none;
+}
+
+q::before, q::after, blockquote::before, blockquote::after {
+  content: "";
+  content: none;
+}
+
+a img {
+  border: none;
+}
+
+article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary {
+  display: block;
+}
+
+sup, sub {
+  font-size: 75%;
+}
+
+sup {
+  vertical-align: super;
+}
+
+sub {
+  vertical-align: sub;
+}
+
+textarea, pre {
+  font-family: monospace;
+}
+
+b, strong {
+  font-weight: bold;
+}
+
+i, em {
+  font-style: italic;
+}
+
+hr {
+  margin: 0;
+  padding: 0;
+  border: 0;
+}
+
+select {
+  font-size: 100%;
+}
+
+img, iframe, video {
+  vertical-align: top;
+}
+
+button {
+  cursor: pointer;
+}
diff --git a/public/assets/scss/global/style.scss b/public/assets/scss/global/style.scss
new file mode 100644
index 0000000..fd8f696
--- /dev/null
+++ b/public/assets/scss/global/style.scss
@@ -0,0 +1,73 @@
+html {
+  scroll-behavior: smooth;
+  font-size: 62.5%;
+}
+
+body {
+  -webkit-text-size-adjust: 100%;
+  font-size: 1.6rem;
+}
+
+.wbr {
+  display: inline-block;
+}
+
+.content-inner {
+  max-width: 1080px;
+  margin-inline: auto;
+  padding-inline: 10px;
+}
+
+#page-container {
+  display: grid;
+  grid-template-rows: auto 1fr auto;
+  grid-template-columns: 100%;
+  min-width: 100dvw;
+  min-height: 100dvh;
+}
+
+#page-header {
+  .content-inner {
+    border-bottom: 1px solid #ccc;
+  }
+  .siteName {
+    padding-block: 8px;
+  }
+}
+
+#page-main {
+  text-align: center;
+
+  .content-inner {
+    > * {
+      margin-top: 30px;
+    }
+  }
+
+  h1 {
+    font-family: serif;
+    font-weight: bold;
+    font-size: 3.2rem;
+  }
+}
+
+#page-footer {
+  border-top: 1px solid #999;
+  background: #f0f0f0;
+
+  .footerArea {
+    padding: 4px 8px 8px;
+
+    .copyright {
+      padding-top: 4px;
+      text-align: center;
+      font-family: Verdana, sans-serif;
+      font-size: 1.4rem;
+
+      img {
+        margin-top: -4px;
+        vertical-align: middle;
+      }
+    }
+  }
+}
diff --git a/public/assets/scss/subpage/style.scss b/public/assets/scss/subpage/style.scss
new file mode 100644
index 0000000..e69de29
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..c35fc5f
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/src/config/Util.mjs b/src/config/Util.mjs
new file mode 100644
index 0000000..9405025
--- /dev/null
+++ b/src/config/Util.mjs
@@ -0,0 +1,35 @@
+const Util = {
+  HTML: {
+    escape(str) {
+      const map = {
+        '&': '&amp;',
+        "'": '&#039;',
+        '"': '&quot;',
+        '<': '&lt;',
+        '>': '&gt;',
+      };
+      return str.replace(/[&'"<>]/g, (m) => map[m]);
+    },
+  },
+  RegExp: {
+    escape(str) {
+      return str.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
+    },
+  },
+  sprintf(format, ...args) {
+    let p = 0;
+    return format.replace(/%./g, function(m) {
+      if (m === '%%') return '%';
+      if (m === '%s') return args[p++];
+      return m;
+    });
+  },
+  ltrim(str, char = '/') {
+    return str.replaceAll(new RegExp(Util.sprintf('^[%s]+', Util.RegExp.escape(char)), 'g'), '');
+  },
+  rtrim(str, char = '/') {
+    return str.replaceAll(new RegExp(Util.sprintf('[%s]+$', Util.RegExp.escape(char)), 'g'), '');
+  },
+};
+
+export default Util;
diff --git a/src/config/myExtIntegration.mjs b/src/config/myExtIntegration.mjs
new file mode 100644
index 0000000..0ec965a
--- /dev/null
+++ b/src/config/myExtIntegration.mjs
@@ -0,0 +1,33 @@
+import fs from 'fs';
+import { globSync } from 'glob';
+import beautify from 'js-beautify';
+
+function htmlFormatter(filePath) {
+  const config = {
+    end_with_newline: true,
+    extra_liners: [],
+    indent_char: ' ',
+    indent_empty_lines: false,
+    indent_inner_html: false,
+    indent_scripts: 'keep',
+    indent_size: 2,
+    wrap_line_length: 0,
+  };
+
+  const inputHtml = fs.readFileSync(filePath, { encoding: 'utf8' });
+  const outputHtml = beautify.html(inputHtml, config);
+  fs.writeFileSync(filePath, outputHtml);
+}
+
+export default function() {
+  return {
+    name: 'myExt:integration',
+    hooks: {
+      'astro:build:generated': (options) => {
+        globSync(`${options.dir.pathname}**/*.html`).forEach((filePath) => {
+          htmlFormatter(filePath);
+        });
+      },
+    },
+  };
+};
diff --git a/src/config/view.mjs b/src/config/view.mjs
new file mode 100644
index 0000000..38eeae1
--- /dev/null
+++ b/src/config/view.mjs
@@ -0,0 +1,45 @@
+import Util from '/src/config/Util';
+
+export const args = {
+  siteName: 'Astro Site',
+  titleSuffix: 'Generated by astro.debiru.net',
+  description: 'Astro Site Starter Kit',
+  twitter: '@YOUR_TWITTER_ID',
+};
+
+export const app = {
+  init(Astro) {
+    app.Astro = Astro;
+    app.args = Astro.props.args ?? {};
+    app.url = Astro.url;
+    args.domain = app.url.hostname;
+    args.path = app.url.pathname.replace(/\.html$/, '');
+    args.url = app.url.origin + args.path;
+    args.cssList = app.args.cssList ?? [];
+    args.jsList = app.args.jsList ?? [];
+    args.titlePrefix = app.args.title;
+    args.title = (args.titlePrefix != null ? args.titlePrefix + ' - ' : '') + args.siteName + ' | ' + args.titleSuffix;
+    args.lang = 'ja';
+    args.locale = 'ja_JP';
+    args.og_type = args.path === '/' ? 'website' : 'article';
+    args.og_image = assetsUrl('img/global/og.png', true);
+  },
+};
+
+export const assets = (path, cacheBuster = false) => {
+  path = '/assets/' + Util.ltrim(path);
+  if (cacheBuster) path += '?' + Date.now();
+  return path;
+};
+
+export const assetsUrl = (path, cacheBuster = false) => {
+  return Util.rtrim(app.url.origin) + assets(path, cacheBuster);
+};
+
+export const img = (path) => {
+  return assets('img/' + Util.ltrim(path));
+};
+
+export const route = (path) => {
+  return '/' + Util.ltrim(path);
+};
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
new file mode 100644
index 0000000..3dc7e73
--- /dev/null
+++ b/src/layouts/Layout.astro
@@ -0,0 +1,58 @@
+---
+import Util from '/src/config/Util';
+import { app, args, assets, img, route } from '/src/config/view';
+app.init(Astro);
+---
+
+<!DOCTYPE html>
+<html lang={ args.lang } id={ args.path }>
+  <head>
+    <meta charset="UTF-8" />
+    <title>{ args.title }</title>
+    <meta name="description" content={ args.description }>
+
+    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
+    <meta name="format-detection" content="telephone=no, email=no, address=no" />
+    <meta name="generator" content={ Astro.generator }>
+
+    <meta property="og:locale" content={ args.locale }>
+    <meta property="og:type" content={ args.og_type }>
+    <meta property="og:site_name" content={ args.siteName }>
+    <meta property="og:title" content={ args.title }>
+    <meta property="og:description" content={ args.description }>
+    <meta property="og:image" content={ args.og_image }>
+    <meta property="og:url" content={ args.url }>
+
+    <meta name="twitter:card" content="summary_large_image">
+    <meta name="twitter:title" content={ args.title }>
+    <meta name="twitter:description" content={ args.description }>
+    <meta name="twitter:site" content={ args.twitter }>
+
+    <link rel="stylesheet" href={ assets('css/global/reset.css', true) }>
+    <link rel="stylesheet" href={ assets('css/global/style.css', true) }>
+    <script src={ assets('js/global/base.js', true) }></script>{
+      (args.cssList.length > 0 || args.jsList.length > 0) && '\n'
+    }{
+      args.cssList.map(css => <Fragment set:html={Util.sprintf('\n<link rel="stylesheet" href="%s">', assets('css/' + Util.ltrim(css), true))} />)
+    }{
+      args.jsList.map(js => <Fragment set:html={Util.sprintf('\n<script src="%s"></script>', assets('js/' + Util.ltrim(js), true))} />)
+    }
+  </head>
+  <body>
+    <div id="page-container">
+      <header id="page-header">
+        <div class="content-inner">
+          <h1 class="siteName"><a href={ route('/') }><img src={ img('global/logo.png') } alt="Astro Site" width="100" height="100"></a></h1>
+        </div>
+      </header>
+      <main id="page-main"><slot /></main>
+      <footer id="page-footer">
+        <div class="content-inner">
+          <div class="footerArea">
+            <p class="copyright"><span class="wbr">Copyright 2025</span> <img src={ img('global/logo.png') } alt="Astro Site" width="30" height="30"> <span class="wbr">Powered by Astro</span></p>
+          </div>
+        </div>
+      </footer>
+    </div>
+  </body>
+</html>
diff --git a/src/pages/index.astro b/src/pages/index.astro
new file mode 100644
index 0000000..624aae9
--- /dev/null
+++ b/src/pages/index.astro
@@ -0,0 +1,13 @@
+---
+import Layout from '/src/layouts/Layout.astro';
+import Util from '/src/config/Util';
+import { app, args, assets, img, route } from '/src/config/view';
+const pageArgs = {};
+---
+
+<Layout args={pageArgs}>
+  <div class="content-inner">
+    <h1>Welcome to Astro Site</h1>
+    <p class="sample-link"><a href={ route('/subpage') }>Subpage</a></p>
+  </div>
+</Layout>
diff --git a/src/pages/subpage/index.astro b/src/pages/subpage/index.astro
new file mode 100644
index 0000000..e474d31
--- /dev/null
+++ b/src/pages/subpage/index.astro
@@ -0,0 +1,16 @@
+---
+import Layout from '/src/layouts/Layout.astro';
+import Util from '/src/config/Util';
+import { app, args, assets, img, route } from '/src/config/view';
+const pageArgs = {
+  title: 'Subpage Title',
+  cssList: ['subpage/style.css'],
+  jsList: ['subpage/script.js'],
+};
+---
+
+<Layout args={pageArgs}>
+  <div class="content-inner">
+    <h1>Subpage</h1>
+  </div>
+</Layout>
```

## git commit しておく（6回目）

```sh
git add .
git commit -m "generate starter kit"
```

## `package-lock.json` を再生成する

GitHub 上での Astro のビルドでは、npm を使用している場合 `package-lock.json` ファイルが使われます。このファイルに問題があるとビルドが失敗するため、念のため再生成しておきます。

```sh
rm -r node_modules package-lock.json
npm install
```

## git commit しておく（7回目）

差分が生じた場合はコミットしておきます。

```sh
git add .
git commit -m "update package-lock.json"
```

## git push する

```sh
git push
```

# おわりに

この手順書で完成するサイトは https://astro.debiru.net/ のようなものです。ソースコードは https://github.com/debiru/astro です。

冒頭でも述べましたが、このスターターキットでは BeautifulCode を実現します。BeautifulCode とは、出力される HTML, CSS, JS のソースコードの可読性を担保することを指します。出力ソースコードの圧縮などをせず、インデントの整った美しいソースコードを提供することを目指します。従来の MPA としての開発アプローチである静的な HTML, CSS, JS の読み込みによりサイトを構築しています。

Astro は SSG (Static Site Generator) として利用できるツールであり、PHP 等のサーバーサイドスクリプトが実行できない GitHub Pages のようなホスティング環境でも、デプロイ時の GitHub Actions を用いて動的に HTML を生成することで各ページで Header や Footer を共通化するなど、プログラマブルな Web サイトの開発を可能にします。

Astro に触れたことのない人でも、このスターターキットを用いて Astro で GitHub Pages を開設し、Astro の魅力を知ってもらえたら嬉しいです。

不明点や疑問、困ったことなどがあれば [@debiru_R](https://x.com/debiru_R) までお気軽にご連絡ください。