どうも、KODANSHAtech のフロントエンドエンジニアです。
このたび、1981年に「Culture Magazine for Your Life」をテーマに創刊した講談社の雑誌『with』を、“親密な時を届けるヴィジュアルインタビュー誌“をテーマにしたデジタルサイト『with digital』として、41年の時を超え2022年12⽉にフルリニューアルしました。

今回、『with digital』には『Astro』を採用しました。
KODANSHAtech が扱うメディアでは、『FRIDAYデジタル』をはじめとして、『Next.js』を利用したSPA構成を取ることが多くあります。
『with digital』は SPA の挙動が必要ではなく、また JavaScript がそこまで必要ではないサイトですが、しかし SPA 界隈で発達した開発手法は使いたかったことが採用の理由です。
また、『with digital』はコンテンツが豊富であり、『Astro』はコンテンツが豊富なWebサイトのために設計されているとのことだったので使ってみたかったということもあります。
基本的には『Astro』は SSG (静的サイト生成) のためのフレームワークなのですが、Node などのランタイムを利用した SSR (サーバーサイドレンダリング) にも対応し、更新の頻繁なウェブメディアのサイトでも使いやすくなったことも採用の決め手となりました。
『Astro』では Island Architecture という、動的なパーツがない場合は Server でレンダリングして、必要なパーツのみクライアントでレンダリングするというアーキテクチャを用いることができます。
これによって、フロントエンド全体としてクライアントから処理を取り除くことができます。
今回は諸事情があり採用できませんでしたが、React を一部分だけクライアントでレンダリングするということもできます。
バックエンドは WordPress を採用しました。WPGraphQL を導入し、GraphQL をインターフェースとしています。また、フロントエンド側では GraphQL Code Generator で自動生成した API を使ってデータを取得する作りとしました。

ディレクトリ構成について

GraphQL の Fragment Colocation を第一に意識したディレクトリ構成になっています。
Fragment Colocation とは、コンポーネントが必要とするデータを Fragment にまとめてコンポーネントと同じ場所に配置 (co-locate) することです。
GraphQL の Fragment とは、複数の Query や Mutation で共有したいフィールドをまとめて 宣言することです。

└ article
  └ article-detail
    ├ ArticleDetail.astro
    ├ ArticleDetail.fragment.graphql
    └ articleDetail.query.graphql

このように、GraphQL クエリファイルが Astro コンポーネントと同階層に設置されています。
以下は実際のコードをサンプル用に加工したコードです。

ArticleDetail.fragment.graphql

fragment ArticleDetail on Post {
  id
  slug
  title
  content
  date
  tags {
    nodes {
      databaseId
      link
      name
      slug
    }
  }
}

ArticleDetail.query.graphql

query articleDetail($articleSlug: ID!) {
  post(id: $articleSlug, idType: SLUG) {
    ...ArticleDetail
  }
}

ArticleDetail.astro

---
import type { ArticleDetailFragment } from '../../../schema/sdk';

export interface Props {
  article: ArticleDetailFragment;
}

---

<article>
  ...
</article>

このように、

  • コンポーネントで利用するデータを取得するクエリを定義
  • データ構造は Fragment として定義
  • コンポーネントを Fragment から自動生成された TypeScript の型を使うように構成

するというのを一貫した開発の型とすることで、静的型チェックを活用しながら効率的に開発を進めることができます。

└ multiple-queries
  └ topPage.query.graphql

こちらは複数のクエリを必要に応じて同時に1つのリクエストで実行するためのディレクトリです。
topPage.query.graphql の中身は以下のようになっています。

query topPage {
  topPageLatestArticles: posts(first: 10) {
    ...ArticleList
  }

  topPagePublicationList: publications(first: 1) {
    ...PublicationList
  }

  topPageLatestPickup: pickups(
    first: 1
    where: { orderby: { field: DATE, order: DESC } }
  ) {
    nodes {
      databaseId
      link
      title
      pickupACF {
        pickupPosts {
          ...PostFieldsForPickup
        }
      }
    }
  }

  topPageCreatorList: creators(first: 6) {
    ...CreatorList
  }

  topPageTagList: displayTags {
    nodes {
      title
      displayTagACF {
        tagList {
          databaseId
          name
          link
        }
      }
    }
  }
}

開発を始めるときは前掲のように Colocation 構造で作っておき、一部 API リクエストに関するパフォーマンス的なボトルネックを対応したい箇所でのみこのようにクエリをまとめてリクエストを効率化することができます。
また、そのときに Fragment を型として使って管理していることで、クエリのチューニング後もその Fragment を引き続き利用することで既存のコンポーネントの実装に影響することなく変更することができます。

ファイル構成について

例として新着記事一覧ページの Astro ファイルを見てみます。

new.astro

---
import NewArticles from '../components/article/new-articles/NewArticles.astro';
import CategorySection from '../components/category/category-section/CategorySection.astro';
import Layout from '../layouts/Layout.astro';
import { createGraphQLClient } from '../lib/graphql-client';
import { getSdk } from '../schema/sdk';

const ITEMS_PER_PAGE = 24;

const graphQLSdk = getSdk(createGraphQLClient());

const url = new URL(Astro.request.url);
const after = url.searchParams.get('after') || null;
const before = url.searchParams.get('before') || null;
const first =
  !after && !before ? ITEMS_PER_PAGE : after ? ITEMS_PER_PAGE : null;
const last = before ? ITEMS_PER_PAGE : null;

const result = await graphQLSdk.newArticles({
  first,
  after,
  last,
  before,
});

const { posts } = result;

const categoriesResult = await graphQLSdk.categories();

const { categories } = categoriesResult;

const pageTitle = '新着記事一覧';
---

<style>
  .title {
    /* 省略 */
  }

  @media screen and (max-width: 768px) {
    .title {
      /* 省略 */
    }
  }
</style>

<Layout title={pageTitle}>
  <section>
    <h1 class="title">NEW</h1>
    {posts && <NewArticles articleList={posts} />}
  </section>
  <CategorySection categoryList={categories!} />
</Layout>

graphQLSdk の部分が WordPress からデータを引っ張ってきている箇所です。
データが記事とカテゴリーに分かれているのがわかると思います。
記事データは NewArticles.astro コンポーネントを、カテゴリーデータは CategorySection.astro コンポーネントをそれぞれレンダリングします。
スタイルは上記サンプルのように <style> タグ内で定義します。
このように、Astro コンポーネントを組み合わせてページを構成することができます。
なお、今回はデザインの都合上採用しませんでしたが、React コンポーネントを組み合わせることもできます。
その場合は、以下の設定で組み込むことができます。

astro.config.mjs

import react from '@astrojs/react';

export default {
  // ...
  integrations: [react()],
}

new.astro

- import NewArticles from '../components/article/new-articles/NewArticles.astro';
+ import NewArticles from '../components/article/new-articles/NewArticles.tsx';

- {posts && <NewArticles articleList={posts} />}
+ {posts && <NewArticles articleList={posts} client:load />}

おわりに

通常のフロントエンド - バックエンド分離構成であれば API ドキュメントの整備 ( Swagger など) の更新が必要だったりしますが、フロントエンドから GraphQL で宣言的にデータを取得できるのは良い開発体験でした。
公式ドキュメントの一部が英語のみだったりと困難はありましたが、その分やりがいもありました。 みなさんもぜひ『Astro』を試してみてください。