はなゐろぐ

主に技術関係の覚え書きです。

Astroを業務で使ってみて

2023年3月1日
Astro
Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.
Astro favicon https://astro.build/
Astro

Astroを触ってみて、これはいいかも…と思い、業務でも何度か使ってみました。感想と、いくつか知見を書いておきます。

所感

まず、Next.jsなどと違い(基本的には)JSフリーの状態でビルドされるのが良いです。「納品後はこっちで更新したい」と言われることも多く、お客様の手で変更できないのはNGな案件が結構多いんですよね…。逆に納品後も保守契約を結ぶなどしてずっと手元で触れるなら、そこまで重要ではなさそう。

記法はかなりとっつきやすいので、コーディングはたまにやる程度のデザイナーとも協業しやすそうです。

---
import '@/styles/global.css';
import { SEO } from 'astro-seo';

import Footer from '@/components/Footer.astro';
import Header from '@/components/Header.astro';
import { config } from '@/config';

export interface Props {
  title?: string;
  description?: string;
}

const { title, description } = Astro.props;
---

<!doctype html>
<html lang="ja" class="">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width" />
    <SEO
      title={`${title ? `${title} | ` : ''}${config.siteMetadata.name}`}
      description={description || config.siteMetadata.description}
    />
  </head>
  <body class="">
    <Header />
    <slot />
    <Footer />
  </body>
</html>

また、getStaticPaths()から直に変数をテンプレートへ渡せるのが良いですね。Next.jsだと別途getStaticProps()で取ってこないといけないので。

Astro Runtime API
Astro Runtime API favicon https://docs.astro.build/ja/reference/api-reference/#getstaticpaths
Astro Runtime API
---
export async function getStaticPaths() {
  const data = await fetch('...').then((response) => response.json());

  return data.map((post) => {
    return {
      params: { id: post.id },
      props: { post },
    };
  });
}

const { id } = Astro.params;
const { post } = Astro.props;
---

<h1>{id}: {post.name}</h1>

個人的にいいなと思ったのは名前付きスロットです。

コンポーネント
Astroコンポーネント構文の紹介です。
コンポーネント favicon https://docs.astro.build/ja/core-concepts/astro-components/#名前付きスロット
コンポーネント
Wrapper.astro
---
import Header from './Header.astro';
import Logo from './Logo.astro';
import Footer from './Footer.astro';

const { title } = Astro.props
---
<div id="content-wrapper">
  <Header />
  <slot name="after-header"/>  <!--  `slot="after-header"` 属性を持つ子要素はここに入ります。 -->
  <Logo />
  <h1>{title}</h1>
  <slot />  <!--  `slot`属性をもたない子要素、`slot="default"`属性を持つ子要素はここに入ります。 -->
  <Footer />
  <slot name="after-footer"/>  <!--  `slot="after-footer"` 属性を持つ子要素はここに入ります。 -->
</div>
fred.astro
---
import Wrapper from '../components/Wrapper.astro';
---
<Wrapper title="フレッドのページ">
  <img src="https://my.photo/fred.jpg" slot="after-header">
  <h2>フレッドについて</h2>
  <p>ここでは、フレッドについて紹介します。</p>
  <p slot="after-footer">Copyright 2022</p>
</Wrapper>

サイドバーにバナーを追加したいとか、このページだけJSを追加したいとか、ちょっとした差分も楽に書けます。

反面う〜んと思ったのは、生成されるCSSファイルのファイル名が適当というか。生成後のコードは触らない前提なので普通は問題ないんですが、is:globalを付けて書いたCSSが404.cssに書き出されたりします。納品後お客様が触る案件では紛らわしいので、CSSを全てグローバルで書き、まとめて書き出されるようにしました。もっとうまいやり方がありそう🤔

astro.config.ts
export default defineConfig({
  vite: {
    build: {
      rollupOptions: {
        output: {
          entryFileNames: `assets/scripts/[name].js`,
          chunkFileNames: `assets/scripts/[name].js`,
          assetFileNames: (asset) => {
            if (/\.css$/.test(asset.name ?? '')) {
              return 'assets/styles/global.css';
            } else if (/\.(jpe?g|png|svg)$/.test(asset.name)) {
              return 'assets/images/[name].[ext]';
            }

            return 'assets/[name].[ext]';
          },
        },
      },
    },
  },
});

追記

Viteのドキュメントを見ていたらCSSを分割するかどうかのオプション項目を見つけました。これをfalseにすることで、style.cssに書き出されます。

export default defineConfig({
  vite: {
    build: {
      cssCodeSplit: false,
    }
  },
});

ちょっとしたテクニック

テクニックというほどでもないんですが、「あれできないの?」をまとめてみました。

下層ページURLを「page.html」にしたい

build.formatfileの場合、src/pages/page.astrodist/page.htmlにビルドされます。

設定方法
設定方法 favicon https://docs.astro.build/ja/reference/configuration-reference/#buildformat
設定方法
astro.config.ts
export default defineConfig({
  build: {
    format: 'file',
  },
});

他の端末からローカルサーバを確認したい

server.hosttrueにすると、スマホや他のPCなどからアクセスできるURLを発行してくれます。

設定方法
設定方法 favicon https://docs.astro.build/ja/reference/configuration-reference/#serverhost
設定方法
astro.config.ts
export default defineConfig({
  server: {
    host: true,
  },
});

astro devするとこんな感じ。

  🚀  astro  v2.0.2 started in 696ms

 Local    http://localhost:3000/
 Network  http://xxx.xxx.xx.xxx:3000/

devとbuildで条件分岐したい

環境変数が用意されていて、それで判定できます。

環境変数
Astroプロジェクトでの環境変数の使い方を学ぶ
環境変数 favicon https://docs.astro.build/ja/guides/environment-variables/#デフォルト環境変数
環境変数

ちなみに環境変数を追加したい場合はプロジェクトディレクトリに.envを作成すれば自動的に読み込んでくれます。

Facebookを埋め込むと動かない

試してないですがおそらくTwitterのツイートボタンもかな? JSでゴニョゴニョするタイプの埋め込みパーツが動かなかったので、set:htmlで。

テンプレートディレクティブの概要
テンプレートディレクティブの概要 favicon https://docs.astro.build/ja/reference/directives-reference/#sethtml
テンプレートディレクティブの概要

set:htmlは要素の中に渡した文字列をHTMLソースコードとして埋め込んでくれます。ブログの本文などに使えますね。<Fragment/>はラッパーを生成せずset:htmlで指定されたコードをそのまま書き出します。

<Fragment
  set:html={`
  <div id="fb-root"></div>
  <script async defer crossorigin="anonymous" src="https://connect.facebook.net/ja_JP/sdk.js#xfbml=1&version=v16.0" nonce="pt9CTwsx"></script>
  <div class="fb-page" data-href="https://www.facebook.com/facebook" data-tabs="timeline" data-width="" data-height="" data-small-header="false" data-adapt-container-width="true" data-hide-cover="false" data-show-facepile="true"><blockquote cite="https://www.facebook.com/facebook" class="fb-xfbml-parse-ignore"><a href="https://www.facebook.com/facebook">Facebook</a></blockquote></div>
  `}
/>

Sitemapから特定のページを除外したい

@astrojs/sitemapインテグレーションで特定のページを除外します。たとえば、外部サービスのテーマをコーディングしたけど公開はしないからSitemapには含めないとか。

@astrojs/sitemap
Learn how to use the @astrojs/sitemap integration in your Astro project.
@astrojs/sitemap favicon https://docs.astro.build/ja/guides/integrations-guide/sitemap/#filter
@astrojs/sitemap

第一引数pageにはhttps://www.example.com/index.htmlのような本番の絶対パスが入っています。これをmatch関数などで当てはまらないものをフィルターして返すようにします。

astro.config.ts
export default defineConfig({
  integrations: [
    sitemap({
      filter: (page) => !page.match(/\/(blog)\/$/),
    }),
  ],
});