Astroでブログ付きのサイトをざっくり理解して作る

## はじめにAstroでブログ+数枚のページをイメージする

[公式のチュートリアル](https://docs.astro.build/ja/tutorial/0-introduction/)でもブログを作るが

- ブログのみの例である
- リファクタリングを経験させるためかなり段階を踏んでいる

ことから、ブログ+数枚のページをつくる際のイメージが「ざっくり」つくような導入にする。

## 対象

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/c8a17362-9677-2a4e-c3fc-c16a0260e181.png)

ブログ記事とは関係ない静的なページ数枚とブログを作りたい時。
例えばブログのシステムでやろうとすると、説明を最上部に表示したく日付を`9999-12-31` に設定するといった記事を魔改造を避けたい。

企業向けのサイトなどや大きいサイトにブログを追加する、複数人で頻繁に編集する用途は向かない。

## Astroの特徴

詳しい説明は [公式のドキュメント](https://docs.astro.build/ja/concepts/why-astro/) やほかの記事で説明しているので、どんなものかをイメージできるよう簡単にまとめる。

- 静的サイトジェネレーター寄り: Webアプリケーションというよりはほぼ固定のコンテンツを届ける
	- SPAではなくMPAを採用し初期の読み込みを抑える
	- 必要に応じてスクリプト(React, Vueなども含む)を追加してインタラクティブ性を持たせることもできる。
- できる限り高速なサイトを生成する: 不必要なJavaScriptを排除し事前にレンダリング(HTMLに変換)する
	- クライアントで読み込む量を減らし、操作できるまでの時間(TTI: Time To Interactive)をなるべく減らす
	- こちらも必要に応じて一部をアクセス毎にレンダリングするようにできる。
- 理解しやすく簡単: HTMLのみでもAstroのページは成立する
	- こちらも必要に応じでヘッダーなどのコンポーネント、ページレイアウトを作成して再利用しやすい形にできる

## プロジェクト作成

ここではTypeScriptを **使用しない**。Astroだけに集中して後程TypeScriptを書くこともできる。

```console
$ npm create astro@latest
astro Launch sequence initiated.

dir Where should we create your new project?
./astro-blog-sample 

tmpl How would you like to start your new project?
Empty

deps Install dependencies?
Yes

ts Do you plan to write TypeScript?
No
```

## イメージを書き起こす

ページの構成は以下の通り。サイトとして数枚のページにブログを追加した構造とする。
- `/`: トップページ
- `/about`: サイトの説明
- `/blog/`: この配下からブログにする
	- `/blog/posts/`: 各記事。Markdownファイルを配置してHTMLに変換する

ファイル構成は以下の通り。 「`components` はAtomic Designに従うこと」などのルールはないため運用しやすい粒度にする。
- `.env`: 作成するサイトの名前を設定する。 サイト全体にわたって使用する設定として(勝手に)定義し `h1`, `title` タグに使用する。

- `./src/components`: 各ページ共通となる部品。 `components` から別の `components` を呼び出すこともできるが今回は
	- `./src/components/Header.astro`: ヘッダー。ここではサイトの名前とナビゲーションを入れる。
	- `./src/components/Footer.astro`: フッター
	- `./src/components/BlogPost.astro`: ブログ投稿一覧のカード1枚

- `./src/layouts`: ページの枠組みとなる部分
	- `./src/layouts/DefaultLayout.astro`: ブログ以外となる枠組み。
	- `./src/layouts/BlogTopLayout.astro`: ブログのトップとなる枠組み。記事を複数並べる。
	- `./src/layouts/BlogPostLayout.astro`: ブログの記事ページとなる枠組み。記事を一つ表示する。

- `./src/pages`: ページ本体となる部分。ここでは `layouts` に `components` やページ固有の内容を組み合わせる。
	- `./src/pages/index.astro`: `/` になる
	- `./src/pages/about.astro`: `/about` になる
	- `./src/pages/blog`
		- `./src/pages/blog/index.astro`: `/blog` になる
		- `./src/pages/blog/posts/`: `post-title.md` という名前でMarkdownを保存すると `/blog/posts/post-title` の形でパスに反映される

## サイトの名前を決める

`.env` を下記の通りに編集する。`SITE_TITLE` という環境変数にサイトの名前を設定してグローバルで使えるようにする。

```./.env
SITE_TITLE=My Astro Site
```

## Component(部品)を作る

各ページ(ブログ記事も含む)すべてに渡って配置されるため、コンポーネント化するとのちの変更が少なくなる。

### ヘッダー

`---` ~ `---` で囲まれた部分を **フロントマター(Frontmatter), コードスクリプト** と呼ぶ。AstroファイルではJavaScriptを記述できる。
`import.meta.env.SITE_TITLE` で `.env` ファイルで定義した `SITE_TITLE`, つまりサイトの名前を呼び出すことができる。

`<header>` ~ `</header>` はHTMLで記述する。
 `{}` で変数名を囲むとフロントマターで定義した変数を使うことができる。
 
`a` タグのリンク先はビルド, デプロイ後のパスを指定する。Astroファイルを指定してビルド時にリンク先を変換することは現状できない。

```html:./src/components/Header.astro
---
const siteTitle = import.meta.env.SITE_TITLE;
---

<header>
    <h1><a href="/">{siteTitle}</a></h1>
    <p>"{siteTitle}" will provide how to make blog with Astro.</p>
    <div class="nav-links">
        <a href="/">home</a>
        <a href="/about">about</a>
        <a href="/blog">blog</a>
    </div>
</header>
```

### フッター

こちらはフロントマターを省略している。HTMLの断片のみで完結する。

```html:./src/components/Footer.astro
<footer>
  <hr>
  <p>Astroを習得するために作られたサイトです。ブログ付きです。</p>
  <ul>
    <a href="/">home</a>
    <a href="/about">about</a>
    <a href="/blog">blog</a>
  </ul>
</footer>
```

### ブログ投稿一覧のカード1枚

少し細かくなるが、ブログ投稿一覧に表示される1記事のカード(下図の点線)をcomponent化する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/216963/77c2f95d-0861-2dd5-eef0-7fc6149d7f80.png)

ここでの `<style>` はAstroでのスタイルタグで、下記の場合はブログ投稿一覧のカード1枚に対するscopedなCSSとして適用される。

`Astro.props` とフロントマター内で指定することでこのcomponentが呼び出される際に値を設定することができる。 `{}` 内(分割代入)でcomponentが受け取る値を設定できる。

```html:./src/components/BlogPost.astro
---
const {url, title, image, description} = Astro.props;
---

<style>
    div {
        border: 1px solid lightgray;
        background-color: rgba(255, 255, 255, 0.5);
        border-radius: 0.1rem;
        padding: 0.5rem;
        margin: 0.5rem 0;
    }
</style>

<div>
    <img src={image.url} alt={image.alt} width="300"/>
    <h2><a href={url}>{title}</a></h2>
    <p>{description}</p>
</div>
```

## Layout(枠組み)を作る

### ブログ以外

ここで `components` に作ったヘッダーとフッターを読み込む。フロントマター内で `components` を **Layoutファイルからの相対パス** で呼び出す。
読み込んだヘッダー、フッターは `<Header />` のように呼び出すことができる。

`components` と同じ呼び出し方で別ファイルのCSSを呼び出すことができる。

`<slot />`にはこのLayout自身を呼び出してページを作る際に内容が入る。
`Astro.props` とフロントマター内で指定することで `<slot />` とは別の値を渡すことができる。値を受け取るように設定する。

```html:./src/layouts/Default.astro
---
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";

import "../styles/global.css";

const siteTitle = import.meta.env.SITE_TITLE;

const { pageTitle } = Astro.props;
---

<html lang="ja">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>{pageTitle} - {siteTitle}</title>
	</head>
	<body>
		<Header />

		<div class="main-wrapper">
			<div class="content-wrapper">
				<slot />
			</div>
		</div>

		<Footer />
	</body>
</html>

```

### ブログのトップ

こちらも同様にヘッダー, フッター, スタイルシートを読み込み、環境変数からサイトの名前を取得する。

```html:./src/layouts/BlogLayout.astro
---
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";

import "../styles/global.css";

const siteTitle = import.meta.env.SITE_TITLE;

const { pageTitle } = Astro.props;
---

<html lang="ja">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
		<meta name="viewport" content="width=device-width" />
		<meta name="generator" content={Astro.generator} />
		<title>{pageTitle} - {siteTitle}</title>

		<style>
			.sidebar-wrapper {
				width: 100%;
				max-width: 30ch;
				padding: 0 1rem;
			}
			.sidebar {
				background-color: rgba(255, 255, 255, 0.5);
				border-radius: 0.1rem;
				padding: 0.5rem;
				margin: 0.5rem 0;
			}
		</style>
	</head>
	<body>
		<div class="root">
			<Header />
			<div class="main-wrapper">
				<div class="content-wrapper">
					<slot />
				</div>
				<div class="sidebar-wrapper">
					<div class="sidebar">
						<h3>サイドバー</h3>
						<form><input type="text"><button>検索</button></form>
						<h3>タグ一覧</h3>
						<p>ここにタグ一覧入る</p>
						<h3>投稿月一覧</h3>
						<p>ここに投稿月一覧入る</p>
					</div>
				</div>
			</div>
			<Footer />
		</div>
	</body>
</html>

```

### ブログの記事

このLayoutはMarkdownで作成したブログ記事が入る。本文は `<slot />` に入る。

Makrdown内のフロントマターも使用することができ、下記の例では `pubDate`, `title`, `image` を用いている。
	`frontmatter.pubDate` はそのまま呼び出すと "2022-08-08T00:00:00.000Z" の形式になるため `slice()` で不要な部分を除去している。

```html:./src/layouts/BlogPostLayout.astro
---
import BlogLayout from "./BlogLayout.astro";

const { frontmatter } = Astro.props;
---

<style>
	.publish-date {
		font-size: 2rem;
		color: gray;
	}

	h2 {
		text-align: center;
	} 

	img.article-image {
		width: 400px;
	}
</style>

<BlogLayout pageTitle={frontmatter.title}>
	<p class="publish-date">{frontmatter.pubDate.slice(0, 10)}</p>
	<h2>{frontmatter.title}</h2>
	<img class="article-image" src={frontmatter.image.url} alt={frontmatter.image.alt} />
	<slot />
</BlogLayout>

```

## スタイルシート

`import "../styles/global.css";` で呼びだされるスタイルシート。

```css:./src/styles/global.css
html {
    background-color: #f1f5f9;
    font-family: sans-serif;
}

body {
    margin: 0 auto;
    width: 100%;
    padding: 1rem;
    line-height: 1.5;
}

hr {
    border: 0.5px solid lightgray;
}

* {
    box-sizing: border-box;
}

h1 {
    margin: 1rem 0;
    font-size: 2.5rem;
}

h1 a {
    text-decoration: none;
    color: black;
}

.main-wrapper {
    display: flex;
    justify-content: center;
}

header, footer {
    margin: 0 auto;
    width: 100%;
    max-width: 80ch;

}

.content-wrapper {
    width: 100%;
}

/* ナビゲーションのスタイル */

.nav-links {
    width: 100%;
    background-color: #ff9776;
    display: none;
    margin: 0;
}

.nav-links a {
    display: block;
    text-align: center;
    text-decoration: none;
    font-size: 1.2rem;
    font-weight: bold;
    text-transform: uppercase;
}

.nav-links a:hover,
a:focus {
    background-color: #ff9776;
}

@media screen and (min-width: 636px) {
    .nav-links {
        display: flex;
        position: static;
        width: auto;
        background: none;
    }

    .nav-links a {
        display: inline-block;
        flex: 1;
        margin: 0;
        padding: 15px 0;
    }

    .content-wrapper {
        max-width: 80ch;
    }

}
```
## Pages(ページ本体)を作る

### ブログ以外の部分: `/`, `/about`

 `./src/pages/` にAstroファイルを配置してはじめてページとして生成される。

Componentを呼び出す要領でLayoutを呼びだす。タグで囲われている中はLayoutの `<slot />` に入る。
また、 `pageTitle` のようにLayoutを呼び出す際に要素として指定すると呼び出されたLayoutで `Astro.props` に値がセットされる。

`index.astro` は `/` に割り当てられる。

```html:./src/pages/index.astro
---
import DefaultLayout from "../layouts/DefaultLayout.astro";
const pageTitle = "Home";
---

<BaseLayout pageTitle={pageTitle}>
	<p>ブログ, プロフィール以外に見せておきたい固定ページ</p>
		
	<img src="https://api.lorem.space/image/album?w=640&h=360" alt="">
	<p>記事の投稿に影響されない成果物など</p>

	<img src="https://api.lorem.space/image/dashboard?w=640&h=360" alt="">
	<p>記事の投稿に影響されない成果物など</p>
</BaseLayout>
```

`about.astro` は `/about` に割り当てられる

```html:./src/pages/about.astro
---
import DefaultLayout from "../layouts/DefaultLayout.astro";
const pageTitle = "About";
---

<BaseLayout pageTitle={pageTitle}>
   <h2>{pageTitle}</h2>
  <p>Astroを習得するために作られたサイトです。ブログ付きです。</p>
  <h2>自己紹介</h2>
  <ul>
    <li>スキル1</li>
    <li>スキル2</li>
    <li>スキル3</li>
  </ul>
  <h2>使用条件</h2>
  <ul>
    <li>画像はAstroのサンプル, <a href="https://lorem.space/">Lorem.space</a>から用いています</li>
    <li>コードはAstroのサンプルを変更したものです。改変自由です。</li>
    <li>そのほか使用条件</li>
  </ul>
</BaseLayout>

```
### ブログのトップ(記事一覧)

`Astro.glob()` で記事本文となるMarkdownファイル群のリストを取得する。こちらは `./src/pages/blog/index.astro` からみた相対パスで指定する。

```html:./src/pages/blog/index.astro
---
import BlogLayout from "../../layouts/BlogLayout.astro";

import BlogPost from "../../components/BlogPost.astro";

const allPosts = await Astro.glob("../blog/posts/*.md");

const pageTitle = "Blog";
---

<BlogLayout pageTitle={pageTitle}>
	{
		allPosts.map((post) => (
			<BlogPost
				url={post.url}
				title={post.frontmatter.title}
				image={post.frontmatter.image}
				description={post.frontmatter.description}
			/>
		))
	}
</BlogLayout>

```
### Posts(ブログ記事)

こちらもPagesと同じ扱いになるのだが、AstroファイルではなくMarkdown形式のファイルを作る。

ブログ記事のタイトル, タグ, 投稿/更新日時, 適用するレイアウトなどはフロントマター内に書く。

次の例は `/blog/posts/my-first-post` としてアクセスできる。
```markdown:./src/pages/blog/posts/my-first-post.md

---
layout: ../../../layouts/BlogPostLayout.astro
title: 私のブログ記事
author: 'ysd-marrrr'
description: "この記事単独で表示されます！"
image:
  url: "https://docs.astro.build/default-og-image.png"
  alt: "惑星と星のイラストの中に「astro」という単語があります。"
pubDate: 2022-08-08
tags: ["astro", "成功"]
---
記事のリストを作成するために`Astro.glob()`がすべての記事データのリストを返しているので、この記事は他のブログ記事と一緒に表示されるはずです。

```


## 動作確認とビルド

動作確認のためにdevサーバーを立ち上げるには次のコマンドを実行する。
ファイルが更新されると開いているブラウザの画面も自動で反映される。 `Ctrl + C` で終了できる。

```console
$ npm run dev
```

ビルドのコマンドは次の通り。

```console
$ npm run build
```