AstroとTailwind CSSを使ってDeveloper Siteをリデザインした話

こんにちは!ウェブサイト課の東尾です。
今年の6月にBacklogをはじめとしたヌーラボの各サービスのAPIを紹介するDeveloper Siteのリデザインをリリースしました。同時期にリリースされたJBUG(ジェイバグ)サイトについて同じウェブサイト課の松永さんが、記事を書かれたため、今回私はDeveloper Siteのリデザインについてお話ししようと思います。
※このブログは、ヌーラバー真夏のブログリレー2024の18日目の記事です。

はじめに

今回のDeveloper Siteリデザインの目的は英語のコーポレートサイト(https://nulab.com/ )とのUIを合わせることでした。旧Developer Siteのヘッダーやフッターなどの共通パーツは古いデザインのままだったため刷新する必要がありました。

 

旧Developer Siteトップページのファーストビューのスクリーンショット旧Developer Siteのトップページ

Developer SiteトップページのファーストビューのスクリーンショットDeveloper Siteのトップページ

使用した技術

今回使用した技術は、静的サイトジェネレータであるAstroとユーティリティファーストのCSSフレームワークであるTailwind CSSです。
旧サイトはHugoとSCSSを用いて構成されており、今回はコンテンツはそのままで見た目のみを変更する制作だったため仕様の変更は不要でした。しかし、パフォーマンスの向上を目指し、かつ新しい技術に挑戦するためAstroとTailwind CSSを採用することにしました。

AstroはJSXライクな書き方ができ、HTMLやJSXを書いたことがある方であれば理解しやすいかと思います。また、AstroにはAstro Islandsというコンセプトがあります。これは、必要な部分だけ動的なJavaScriptを後から読み込むことで、初期ロード時間を短縮し、サイト全体のパフォーマンスを向上させる仕組みです。

Tailwind CSSはシンプルなデザインに対応できるユーティリティクラスが豊富であったため採用しました。

ちなみに私はAstroを触ったことはありましたが、Tailwind CSSの使用は今回が初めてでした。(ナイスTryFirst!)

ファイル構成

ファイル構成は以下の通りです。

├── public/
│  ├──ogp.png
├── src/
│  ├── assets/
│  │  └── images/
│  ├── components/
│  │  ├── layout/
│  │  ├── page/
│  │  └── ui/
│  ├── content/
│  ├── i18n/
│  ├── layouts/
│  ├── pages/
│  ├── styles/
│  ├── types/
│  └── utils/
├── astro.config.mjs
├── package.json
├── tsconfig.json
└── tailwind.config.mjs

src/components

今回はデザインがシンプルだったため、Atomic Designの構成にはせず、layoutpageui というカテゴリで分けました。開発当初は、コンポーネントがそこまで多くならない想定だったため、ディレクトリを分けておらず、 components 配下に全て設置していました。実装を進める中でコンポーネントが想像より増えそうという結論からディレクトリを分けるようにしました。途中でウェブサイト課のメンバーとどのようなディレクトリ構成が適切かを議論できたのはよかったです。

layout:ページ全体のレイアウトを担うヘッダーやフッターなどの要素を格納しています。
page:各ページを表すコンポーネントを格納しています。今回のサイトは多言語対応のため、ルーティング用の/src/pages/は責務をルーティングのみとし、共通のレイアウトを /src/components/page/ に格納しました。
ui:ButtonやCardなどのパーツを格納しています。uiディレクトリの中でも search などのようにさらにディレクトリを切って格納しています。

src/content

こちらのディレクトリには、Markdownファイルを格納しています。旧サイトでは、各ドキュメントの記事がMarkdownファイルで管理されており、今回は内容をそのまま移行する必要がありました。元々のフロントマターはtoml形式で記述されていましたが、Astroで使用するためにyaml形式に変更しました。
加えて、移行に伴い型定義の設定も行いました。Astroには型定義ができるコンテンツコレクション機能というものがあります。型定義をしておくことで、コンテンツが正しい形式で記載されているかをチェックしてくれます。これにより、ミスやデータの不一致を防ぐことができます。

src/layouts

Astroがデフォルトで用意しているディレクトリです。複数のページで共有するためのUIを格納しています。

src/styles

Tailwind CSSを使用しているため、スタイルは基本的にクラス名で指定していますが、Markdownファイルやドキュメントページで使用している目次生成ライブラリTocbotのスタイルの上書きをするため、独自のファイルを作成しました。

tailwind.config.mjs

Tailwind CSSをカスタマイズするための設定ファイルです。独自クラス名の登録やブレイクポイントの設定など、デフォルト設定を上書き・追加できます。

src/i18nsrc/pagesに関しては次の「多言語対応」にて解説します。

実装について

多言語対応

Developer Siteには英語/日本語のページがあり、多言語対応が必要でした。今回はデフォルト言語が英語のため、日本語ページの場合はURLに ja のプレフィックスを設定しました。
ルーティングの設定はとても簡単で、下記のように astro.config.jsi18nの項目を記載するだけです。あとは、ルーティングの役割を担う /src/pages/ 内に /ja/ を作ることで、ルーティングが完了します。

export default defineConfig({
 i18n: {
   defaultLocale: "en",
   locales: ["en", "ja"],
   routing: {
     prefixDefaultLocale: false
   }
 },
})

prefixDefaultLocalefalse にすることで、デフォルト言語の場合はURLに言語プレフィックス(例: /en/〇〇/ )が入らないようになります。

レイアウトは同じで、内容は英語/日本語と変更したかったため、i18n(国際化)*用のファイルをjsonファイル形式で用意し、URLによって表示する言語を切り替えていました。

"description_base": {
  "en": "Build fantastic apps using Nulab APIs and get support from our developer community.",
  "ja": "APIでヌーラボサービスと連携。チームにさらなるコラボレーションを。"
},

現在いるページがどちらの言語かは Astro.currentLocale で取得できます。

---
import homeData from '@src/i18n/home.json'
const language = Astro.currentLocale === 'en' ? 'en' : 'ja'
---
<Heading level="h1" className="mt-2 text-center">
  {homeData.heading[language]}
</Heading>

※i18nは internationalization (国際化対応)の略です。

検索機能

Developer APIサイトには、検索機能があり今回はpagefindを使用しました。pagefindはビルドされた静的ファイルから全文検索ができ、軽量です。
pagefindのデフォルトUIでもカテゴリの絞り込みは可能ですが、以下の実装を行うために、JavaScript APIを使用しました。JavaScript APIを使うことで、絞り込みやスタイリングをカスタマイズすることができます。

  • 各プロダクト毎の検索結果ページを表示
  • ヘッダーに検索機能があるため、結果ページへの遷移が必要
  • ページ遷移をしないページネーションのため、全ての結果を取得した後に表示する結果の調整が必要

読み込み

ビルド後に全文ファイルが /dist/ 内(Astroのデフォルトビルドディレクトリ)に生成されるため、開発環境時は出力先--output-path )を public に指定し、 /public/ 内に生成されるようにしました。
下記のように window オブジェクトに pagefind をグローバル変数として登録し、インポートしています。

<script is:inline>
  window.addEventListener('DOMContentLoaded', async () => {
    if (typeof window.pagefind === 'undefined') {
      try {
        window.pagefind = await import(`/pagefind/pagefind.js`)
        window.dispatchEvent(new Event('pagefindLoaded'))
      } catch (e) {
        window.pagefind = { search: () => ({ results: [] }) }
        window.dispatchEvent(new Event('pagefindLoaded'))
      }
    }
  })
</script>

フィルタリング

検索対象はdata-pagefind-bodyを用いてドキュメントページのみに限定しました。ドキュメントページは動的ルーティングにより、全プロダクトのページが生成されるため、data-pagefind-filterを使って各プロダクトごとにフィルター値を設定しました。( ${category} には各プロダクトの識別子が入ります。)
また、マークダウンファイル内で archivetrue になっている場合は、検索対象から外すためにdata-pagefind-ignoreを使用して除外しました。

▼src/pages/[…slug].astro

---
import { getCollection } from 'astro:content'
import type { InferGetStaticPropsType } from 'astro'
import Markdown from '@src/components/page/Markdown.astro'

export async function getStaticPaths() {
  const allDocuments = await getCollection('document')

  return allDocuments.map((entry) => ({
    params: { slug: entry.data.url },
    props: { entry },
  }))
}

type Props = InferGetStaticPropsType<typeof getStaticPaths>
const { entry } = Astro.props as Props;
const { Content } = await entry.render();

---

<Markdown Content={Content} entry={entry} />

▼/src/components/page/Markdown.astro

---
const { Content, entry } = Astro.props;
const category = Object.keys(entry.data.menu)[0]
---

<div data-pagefind-body>
  <div data-pagefind-filter={`products:${category}`}>
    <div data-pagefind-ignore={entry.data.archive}>
      <Content />
    </div>
  </div>
</div>

※コードは一部抜粋です。コード内のgetStaticPathsgetCollectionについては公式ドキュメントをご覧ください。

検索結果の表示

検索ボックスに入力された文字列をクエリパラメータとして送信し、その値をpagefindに渡しています。キーワードに該当する、各プロダクトの結果をリストとして取得しています。

const documents = await (window as any).pagefind.search(
  '検索されたキーワード', {
  filters: { products: 各プロダクト識別子},
})
// documentsには結果一覧が取得されます。

取得された結果リストの各要素から、以下のようにメタデータを取得し表示しています。

const oneResult = await documents.results[0].data();

// oneResultの内容
{
 "url": "〇〇",
 "excerpt": "〇〇",
 "filters":products: ['各プロダクト識別子']
 "content": "〇〇",
 "meta": {
   "title": "",
 }
}

アクセシビリティ対応

リデザインに加えて、下記のアクセシビリティ向上対応を行いました。

  • 適切なHTML構造でのマークアップ
    • スクリーンリーダーでの検索性と読みやすさの改善
  • フォーカスインジケーターの表示
    • キーボード操作時の現在位置がわかるように
  • remを使用した実装
    • ブラウザのフォントサイズを200%に設定しても見た目が崩れないよう改善
  • aria-current属性の使用
    • スクリーンリーダーで現在位置を正確に把握可能に

 

ドキュメントページのスクリーンショット。フォーカスインジケーターが表示されており、検索要素にフォーカスされていることが視覚的にわかる。フォーカスインジケーターが表示されており、検索要素にフォーカスされていることが視覚的にわかる。

ウェブサイト課では今熱意を持ってアクセシビリティの向上に取り組んでいます。ぜひアクセシビリティーレポートもご覧ください!(ヌーラボ アクセシビリティレポート#1ヌーラボ アクセシビリティレポート#2ヌーラボ アクセシビリティレポート#3

余談

本プロジェクト開始時は、「Developer Siteリニューアル」と言っていましたが、海外メンバーに質問したところ「Developer Siteリデザイン」の方が正しいようです。確かに内容は変えず見た目のみを変更するため「リデザイン」の方が適当だなと思いました。

おわりに

今回Techブログというものを初めて書きました。ブログを書くにあたり、改めてドキュメントやコードを見返しました。結果Astroについての知識が書く前よりも定着した気がします。また、Astroを触るようになってから、絵文字でロケットマーク(🚀)を使う頻度が増えました。Astroのロゴがロケットデザインのため、無意識的に親近感を覚えたようです。
Astroをここまで深く触ったことがなかったため、今回のプロジェクトは間違いなく私にとって大きな影響となりました。Astroを触ったことがない方でも、HTMLやJSXの知識があれば理解しやすいかと思います。
今回のブログがAstroを使うきっかけになれば幸いです!

開発メンバー募集中

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる