Closed18

Astroで普通にWebサイトを作る際のメモ

ItahanaItahana

スクラップで書いた方がいいと思ったのでこっちでやってから記事にします。

ItahanaItahana

https://docs.astro.build/en/getting-started/

事前調査などは一通り終えていて、簡単なサンプルなどは試してある状態からスタート。

インストールして設定していく

とりあえず普通にインストールしていきます。

yarn create astro

sitemap、compressあたりは必要なのでインストールし、後から必要に応じて画像関連やその他SEO関連のインテグレーションをインストールしていく流れです。

distされるファイルのoutput部分をまとめてしまいたいので、下記のようにファイルタイプごとにディレクトリに収まるようにastro.config.mjsを調整します。

astro.config.mjs
export default defineConfig({
  integrations: [sitemap(), compress()],
  // ディレクトリ整理
  vite: {
    build: {
      rollupOptions: {
        output: {
          assetFileNames: (assetInfo) => {
            let extType = assetInfo.name.split('.').at(-1);
            if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
              extType = 'images';
            }
            if (/ts|js/i.test(extType)) {
              extType = 'script';
            }
            if (/css|scss/i.test(extType)) {
              extType = 'css';
            }
            return `assets/${extType}/[name]-[hash][extname]`;
          },
        },
      },
    },
  },
});
ItahanaItahana

ページファイルを作成していく

astroはFront-matter部分にscriptを書いていくんですが、ReactやVue(script setup)同様に肥大化しやすいので注意が必要だなと感じますが、json-ldなどのデータをページごとに持つ場合はページ情報の変数をそれぞれのページで定義する必要もあります。

それ自体も別のtsやjsonで管理してページファイル名ベースで呼び出しても良いかと思いますが、ページ数が多くなると作業漏れが発生する可能性もあるので塩梅が難しいですが、こちらも数ページ作ってみて考えたいと思います。

下記のようなデータをLayoutファイルにPropsで渡してHead内に反映するイメージです。

/blog/index.astro
/**
 * ページ設定
 */
const { pathname } = Astro.url;
const page = {
  title: 'ブログ',
  description: 'ブログの説明',
  schema: [
    {
      '@type': 'ListItem',
      position: 1,
      name: 'トップ',
      item: `${SITE_URL}`,
    },
    {
      '@type': 'ListItem',
      position: 2,
      name: 'ブログ',
      item: `${SITE_URL}${pathname}`,
    },
  ],
};

ページとレイアウトファイルを用意しながら必要そうな機能を共通化を前提に用意していきます。

/features/api/fetchBlog.ts
import {fetchApi} from '@/features/api/fetchApi'

// TODO: env
const endpoint = 'XXXXXXXX'

// 全件
const fetchBlogs = async () => await fetchApi(endpoint)

// 詳細フィルター (単一)
const fetchBlog = async (slug?: string) => {
  const blogs = await fetchBlogs();
  return blogs.filter((item: { slug: string | undefined }) => item.slug === slug)[0]
}

一覧ページの記載は下記のようになります。
<li>タグあたりは後々コンポーネントにしたい部分ですね。

/blog/index.astro
---
import { fetchBlogs } from '@/features/api/fetchBlog';
import { SITE_URL } from '@/constants';

import Layout from '@/layouts/Layout.astro';
import FormattedDate from '@/components/FormattedDate/FormattedDate.astro';

/**
 * ページ設定
 */
const { pathname } = Astro.url;
const page = {
  title: 'ブログ',
  description: 'ブログの説明',
  schema: [
    {
      '@type': 'ListItem',
      position: 1,
      name: 'トップ',
      item: `${SITE_URL}`,
    },
    {
      '@type': 'ListItem',
      position: 2,
      name: 'ブログ',
      item: `${SITE_URL}${pathname}`,
    },
  ],
};

// APIからデータ取得
const blogs = await fetchBlogs();

---
<Layout title={page.title} description={page.description} schema={page.schema}>
  <h1>{page.title}</h1>
  <p>{page.description}</p>
  <section>
    <ul>
      {
        blogs.map((blog: BlogItem) => (
          <li>
            <a href={`/blog/${blog.slug}/`}>
              <div>
                <img
                  src={blog.image}
                  width="300"
                  height="200"
                  alt={blog.title}
                />
              </div>
              <FormattedDate date={blog.date} />
              <p>{blog.title}</p>
            </a>
          </li>
        ))
      }
    </ul>
  </section>
</Layout>

詳細ページの処理に関してはページファイルでのみしか使わないのでページファイル側に処理を記載します。
json-ld用のスキーマの詳細ページタイトルなどは取得したデータを入れるように調整すると良いと思います。

/blog/[...slug].astro
---
import { fetchBlogs, fetchBlog } from '@/features/api/fetchBlog';
import { SITE_URL } from '@/constants';

import Layout from '@/layouts/Layout.astro';
import FormattedDate from '@/components/FormattedDate/FormattedDate.astro';

/**
 * ページ設定
 */
const { pathname } = Astro.url;
const page = {
  title: 'ブログ詳細',
  description: 'ブログ詳細の説明',
  schema: [
    {
      '@type': 'ListItem',
      position: 1,
      name: 'トップ',
      item: `${SITE_URL}`,
    },
    {
      '@type': 'ListItem',
      position: 2,
      name: 'ブログ',
      item: `${SITE_URL}/blog`,
    },
    {
      '@type': 'ListItem',
      position: 3,
      name: 'ブログ詳細',
      item: `${SITE_URL}${pathname}`,
    },
  ],
};

/**
 * 詳細ページ設定
 */
export async function getStaticPaths() {
  const blogs = await fetchBlogs();
  return blogs.map((blog: any) => ({
    params: {
      slug: blog.slug,
    },
  }));
}

/**
 * 記事の詳細情報を取得
 */
const { slug } = Astro.params;
const post = await fetchBlog(slug);

---
<Layout>
  <article>
    <h1>{post.title}</h1>
    <FormattedDate date={post.date} />
    <div>
      <p>{post.content}</p>
    </div>
  </article>
</Layout>
ItahanaItahana

schemaはやはりページに置くとボリューム感が否めない。
グロナビとかの設定もつけて別ファイルの方が利便性はありそうだけど…。

hygenで生成するようにしてしまえば必ず設定ファイルから作業スタートにできそうな気がするのでそういう視点で考えておこう。
CI上で実行させたらCMSでページ作成とかもできるかもしれない。

ItahanaItahana

主題と関係ないけどplopjsってのもあるんですね。
ejs書きたくないって俺みたいなのはこっちのがいいかも。
https://plopjs.com/

ItahanaItahana

今回は特にデザインを考えていなかったので、TailwindCSSベースのdaisyUIを入れてみます。

TailwindとdaisyUIインストール

https://docs.astro.build/ja/guides/integrations-guide/tailwind/

まず、Astro公式にあるインテグレーションからTailwindCSSをインストールしていきます。

yarn astro add tailwind

次にdaisyUIをインストールします。

yarn add daisyui

そうすると、tailwind.config.cjsが作られるので、このファイルでdaisyUIの設定も行っていきます。
ついでに@tailwindcss/typographyもインストールしたので、下記のようになります。

tailwind.config.cjs
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
  theme: {
    extend: {},
  },
  plugins: [require('@tailwindcss/typography'), require('daisyui')],
  daisyui: {
    logs: false,
    themes: ['dark', 'winter'],
  },
};

https://daisyui.com/docs/config/
設定項目は上記を参照してください。
ターミナルにログが出るのがうるさく感じたので僕はログをオフにしました。

またライトモードとダークモードの切り替えもできるようにしてみようと思い、
2つのテーマを設定しています。

テーマ切り替え

https://daisyui.com/docs/themes/
テーマ切り替えをしない場合にはdaisyUIのドキュメントに記載の通り、htmlタグに対して
data-theme="dark"のように設定をすればテーマが反映されます。

<html data-theme="dark">
  <div data-theme="light">
    This div will always use light theme
    <span data-theme="retro">This span will always use retro theme!</span>
  </div>
</html>

theme-changeインストール

https://www.npmjs.com/package/theme-change
テーマ切り替えはこちらを使用しました。
npmからインストールし、紹介されている下記のようなコードをLayoutファイルなどに書き込みます。

<script is:inline>
  if (localStorage.getItem('theme') === null) {
    document.documentElement.setAttribute('data-theme', 'light');
  } else {
    document.documentElement.setAttribute(
      'data-theme',
      localStorage.getItem('theme')
    );
  }
</script>
<script>
  import { themeChange } from 'theme-change';
  themeChange();
</script>

切り替えボタンはdaisyUIのコンポーネントのswapにちょうど良いものがあるので、そちらを使用します。

<label
  class="btn btn-square btn-ghost swap swap-rotate"
  data-toggle-theme="winter"
  data-act-class="swap-active"
>
  <svg
    class="swap-on fill-current w-6 h-6"
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    ><path
      d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"
    ></path></svg
  >
  <svg
    class="swap-off fill-current w-6 h-6"
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    ><path
      d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"
    ></path></svg
  >
</label>

これで簡単にテーマ切り替えが可能で、Tailwindの書き方もでき、ある程度UIが揃っているdaisyUIを使うことができるようになりました。

ついでにWebフォント読み込み

高速なサイトを目指したいけどWebフォントは使いたいなということもあるとおもうので、ひとまず簡単な方法をご紹介します。

@fontsource を使うと簡単にWebフォントを使えます。
https://fontsource.org/
こちらのサイトで使いたいフォントを検索します。

目当てのフォントが見つかったらページを開くと、インストール手順が記載されていますので、
コマンドでインストールし、記載の通りCSSを設定すれば簡単に使うことができます。

高速な読み込みなのかは検証していないですが、どうしても気になる場合には遅延読み込みの方法にすると良いかもしれません。

<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap" as="style">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap" media="print" onload="this.media='all'">
ItahanaItahana

daisyUIを入れてみての感想。
管理画面とかアプリとしての画面なら全然事足りそうではあるが、組み合わせて使う場合にHTML構造的に要件を満たせず、結局は自分で書かなければならない部分があると感じた。

まあこれはどのUIフレームワークでも同じようなものだし、レイアウトや基本的な画面仕様はそれぞれあるのでしょうがない部分ですね。

その観点でいうと、daisyUIはレイアウト部分などがほとんど設計されていないので、独自に考えやすく用意されているものが邪魔になったり無駄になりにくいと言えなくもない。

好みの問題かもしれないけど、その他TailwindベースのUIフレームワークを選ぶ場合には下記など参考にしてみてください。
まとめて下さっている方がいて助かりますね。
https://zenn.dev/kkeisuke/scraps/c3d668e6388676

ItahanaItahana

ここから先は記事としてそれほど価値がない部分の話になりそう。
コンポーネントの用意とひたすらページを作るだけではあるので。

それはそうと、ページネーションってみんなどうやって作ってるんだろう。
Jamstack的にはAPI側からtotalを取得しページ掲載件数で割ってページ数算出してページャーの設定にして、掲載件数ずつ取得して静的に生成って流れだろうか。

ついでに検索ってみんなどうしてるんだろう?サービス使うっていうのはコストかけれる場合にはいいんだけど。

個人的な手法としては記事データを一度取得してjson化しているんだけど、これをページ生成と同時にやるとどうしてもページに直接関係ない処理を混ぜなきゃいけないので今は事前にやっている。
CMS側が検索APIを提供している場合はそっちを使うこともできるけど、せっかくJamstackでクライアントサイドからのアクセスでCMSのAPIと切り離しているのに。と思ってしまうが致し方ない部分なのだろう。

ということで、下記2点での手法を抑えておくっていうことで個人的に落ち着いた。

  • ローカルファイル型
  • 検索API型

そこで普通のメディアサイトとかだと検索結果ページを用意する必要があるけど、その場合にはページネーションの処理はクライアントサイドで行う必要があり、ここはSPA状態になる。
その際に配列を任意の数で分割する関数を用意しているけど、下記は非常に参考になりますね。
https://qiita.com/STSHISHO/items/e50b239927605114742d

こちらも少し違うアプローチですが、順序立てて説明しています。
https://yucatio.hatenablog.com/entry/2019/12/10/222311

なんだか配列強者な方々ってすごいですよね。

ItahanaItahana

そうか、Astroにはpaginateがあるのか。
となると自前で用意しなくてもいいってことなんですね。

ItahanaItahana

それでもindexと/page/1はそれぞれ分かれてしまうか。
記事リストのコンポーネントを作るのは気持ち悪いし、
URLが/page/1ができるのも気持ち悪いんだよね。
これいい感じにできる方法はあるのだろうか。

ItahanaItahana

ちょっとちゃんとサイト作ってみているのだけれど、
jsonをimportしてgetStaticPathsに食わせてparamで受け取ってってやっていたんですが、
このjsonを使ってリンクも作っちゃおう!その配列にTOPへのリンクも混ぜちゃおう!
じゃあunshiftで配列に入れちゃえばいいか!って思って変数に入れてから追加したらrouteに影響しちゃったんですが。

つまり、importしたjsonをどこかで持っているんだと思うんですが、何も考えずunshiftとかするとそのデータに追加されてしまうので、deep copyしなきゃいけないよってことでした。

ローカルでのSSR状態では気づかなかったけどbuildログでなんかおかしいことに気づいた。

ItahanaItahana

あとcompress使うとDaisyUIのダークモードのテーマ設定がビルド時にTailwindよりも後方に行ってしまいメディアクエリのprefers-color-schemeが優先されてしまうので、CSSだけ別の圧縮にしようかな。

https://github.com/astro-community/astro-compress#readme

ItahanaItahana

少し勘違いというか、物理的にファイルを置くことに依存していた気がする。
ルーティングはスプレッドで指定をしておけば動的に階層に対しても設定ができるはずだよね。
つまり、インデックスもカテゴリー一覧もタグ一覧も識別できる静的なディレクトリを挟んでも1ファイルで表現ができるはず。

Nuxtでも同じようなことをやって処理がかなり面倒になったキヲクと共に禁じ手的に自分の中で封印していたけど、Astroの場合はページ側で行える処理が逆に少ないので、明確なルーター制御を外部で書いてしまえば肥大化することなく一つのテンプレートで賄えそう。

そうしたらページ側ではpropsで記事データだけを受け取ってあとはコンポーネントに渡していくという、見た目的には良い感じにできるのではないだろうか。

初歩的な話ではあるんだろうけど、Vueから入ったからこそページで頑張る思考に陥ってた気がする。

ItahanaItahana

時間が開いてしまったが、複数階層の動的ルートもできた。

ただ細かい部分で問題があって、page数とcategoryをマッチさせた場合、indexではpage数とcategoryがない状態になるので、getStaticPaths()で返す[...path]に当たるものがない状態になる。

[...path].astro
// マッチさせたいpath
/blog
/blog/page/2
/blog/category/xxxxx
/blog/category/xxxxx/page/2

そこで公式のドキュメントを見たらparamの記述としてundefinedを指定すればindexになるとのこと。
https://docs.astro.build/en/core-concepts/routing/#rest-parameters

そして設定したものの、Typescript的にはpathはStringなのでundefinedだと型エラーを返してくる。
// @ts-ignoreで凌いでいいのじゃないかとは思うが、きっちりしておきたいならgetStaticPathsの戻り値の型をオーバーライドなどすればいけるだろうけど、公式が対応してくれるとありがたいですね。

ItahanaItahana

あとはコンポーネントに関してをまとめる。

静的コンポーネントとしてはAstroのコンポーネントはそれなりに書きやすいと思うけど、突然ここだけSPAにしたい!っていう場合にはゴリゴリとjsを書かなきゃいけなくなるので、それであれば始めからReactでコンポーネントを書いておいて、用途によって静的か動的かを選べばいいんじゃないか。という気がしている。

なぜそう至ったかというと、動的にしたい場合にReactなどを入れるとAstroコンポーネントを動的に使うことができなかったため。(やり方がわるいのかな?)
とにかくclientディレクティブを全部試してみたけどAstroコンポーネントの読み込み自体エラーになる感じなので、Reactコンポーネントを別途用意する他なかった。

という流れで、「それなら動的になり得るものはReactにしておけばいい」ということを思った次第。

デザイン的都合のコンポーネントはAstroで良いと思うし、突然SPA状態にしたければ処理用コンポーネントとしてReactを入れて、その中でcardなどを呼び出せばいい。

ItahanaItahana

それはもうNext.jsでいいのでは?
という気持ちもわかるけど、最初は完全静的でお手軽にスピーディーにAstroで作ればいいって思って始めても、あとから要件が増えた場合に絞り込み検索レベルの実装のためにNext.jsで書き直すのは面倒だという話ですね。

最初からNext.jsでいいのでは?
という話で行くと、歩いて1分のコンビニに車で行くようなもので、免許があってたくさん買い物をする可能性を考えたら車はいいんだけど、免許持ってない人は車で行けないし、買い物が少ない人もいるって話ですね。

このスクラップは2023/09/05にクローズされました