会社サイトリニューアルの検討と実際:実装編 第1回 データ・コンテンツ・レイアウトの分離

2023年に行ったピクセルグリッドの会社サイトリニューアルでは、データの再利用性や、コンテンツ編集者にとっての可読性を大切に、内部のコード構造も大幅リニューアルしています。まずはどのような意図でデータ設計を行ったのかを紹介します。

発行

  • 菅野 亜実
  • 矢倉 眞隆
会社サイトリニューアルの検討と実際:実装編 シリーズの記事一覧

はじめに

2023年7月12日に、ピクセルグリッドの会社サイトがリニューアルされました。

本連載は技術編と称して、サイトを見ているだけでは気づきにくい、このリニューアルの実装に関するお話をしたいと思います。この記事では、冒頭の「採用した技術」では、フロントエンド・エンジニアの矢倉が、それ以降の具体的な実装についてはフロントエンド・エンジニアの菅野が執筆します。

なお、このリニューアルでは、サイトのデザイン自体も変えており、それについては、デザインの視点でお話しているシリーズがあります。

採用した技術

まずは、採用した技術を概観していきましょう。

ピクセルグリッドの会社サイトでは、以下のフレームワークやプラットフォームを採用しています。

  • Astro:静的サイトジェネレータとして利用
  • CSS:SassからプレーンなCSSに変更
  • MDX:コンテンツ管理用に新規導入
  • Svelte:フォームなど、クライアントサイドのJavaScript埋め込み時に利用
  • Cloudflare Pages:ホスティングサービスとして利用
  • Cloudflare Pages Functions:フォーム送信データを受け取るAPIとして利用

新しく利用したのはMDXのみになりますが、利用している技術のバージョンアップに伴う新機能の追加もあり、構成は変わっています。本シリーズで特筆したいのは、新たに導入したMDXの使いどころ、Astroのバージョンアップによる新機能の利用と構成の変化、Sassを撤廃してプレーンなCSSで書くようになった点です。

まず初めに、どのような技術をどのような意図で使ったのか、全体を見てみます。

Astro

静的サイトジェネレーターとしてAstroを採用しています。ピクセルグリッドではAstroの開発が始まった直後から注目しており、会社サイトでは2022年の初頭にそれまで使っていたEleventyから移行しました。

会社サイトはほぼすべてが静的なコンテンツであり、SPAやSSRといった構成を必要としません。また、過剰なクライアントサイドJavaScriptの使用も好んではいません。

ビルドされたページがシンプルな作りである一方、コンポーネントベースのフレームワークであり、他のクライアントサイドJavaScriptライブラリの組み込みも容易であるといった利点から、Astroを継続採用しました。

リニューアル以前のサイトのリポジトリは、Astro移行前のEleventy時代の技術構成を残していました。今回のリニューアルでは見た目はもちろん、データの取り扱いについても見直しを行いました。

CSS

CSSについては、長らくSass(SCSS)を利用していました。特に不満もなく、Sassの各種機能に恩恵を受けていたのですが、リニューアルにあたり、プレーンなCSSで書くことにしました。これについてはCSS設計の回で触れたいと思います。

MDX

これまで各ページはAstroファイルにコンテンツのHTMLを直書きしていましたが、リニューアルにあたってページのコンテンツは基本的にMDXで記述するようにしました。

といってもMDXでは表現しにくい複雑な構造(アクセスのページなど)もあり、それらはマークアップしたAstroコンポーネントを埋め込むかたちで実現しています。

Cloudflare Pages

ピクセルグリッドの会社サイトは、Cloudflare Pagesを、このサービスが一般公開された2021年4月から利用しています。パフォーマンスの高さや運用にかかるコストの安さが魅力です。提供されている機能は当初はシンプルでしたが、その後Functionsの統合をはじめとした機能拡張やビルド速度の向上など、安定して成長しているサービスです。

Svelte + Cloudflare Pages Functions

ピクセルグリッドの会社サイトは基本的に静的サイトですが、お問い合わせフォームなど、一部JavaScriptを利用している部分もあります。AstroではこうしたクライアントサイドのJavaScriptは、インテグレーションを介して他のライブラリを使うようになっています。

会社サイトでは、バンドルサイズの小ささ、実行速度の速さといった観点からライブラリとしてSvelteを採用しています。リニューアル以前から使っていたコンポーネントのスタイルを更新するかたちで使っています。

フォームから送信されたデータを受け取るAPIはCloudflare Pages Functionsを利用しています。こちらもリニューアル以前から使っていたコードを使っています。

その他

Astroや既存のツールでは、賄えない機能もあります。たとえば次のようなものです。

  • OGP画像生成
  • バリアブルフォントのサブセット生成

こうした機能については、機能を実行するスクリプトをAstroのインテグレーションとして実装しました。

データ設計から一新したリニューアル

それでは、今回のリニューアルでの各ポイントを詳しく見ていきます。まず始めは、Astroによるデータ設計です。この会社サイトでは、ヘッドレスCMSを採用せずに、すべてリポジトリ内のデータを元に生成しています。

Astro v2.0で、コンテンツコレクション(Content Collections)という、MarkdownもしくはMDXによるコンテンツ管理機能が追加されました。また、v2.5ではコンテンツコレクションがさらに進化し、YAMLファイルやJSONファイルも正式にサポートされました。

この流れに乗り、今回の会社サイトリニューアルでは、データやコンテンツをどう管理するかという部分から見直すことにしました。

まず、Webサイトを構成する情報を整理し、おおむね、以下の3つの分類を意識しています。

  • ページとは独立したデータ(「スタッフ」や「ニュース」などの情報)
  • ページ固有のコンテンツ
  • ページのレイアウト(サイトの表示を定める基本ルール)

ここから目指したのは、コンテンツとレイアウトを分離し、データを特定のページに依存させない設計です。

ページのコンテンツとレイアウトの管理をMDXで改善する

今回のリニューアル前までは、コンテンツは.astroファイルのマークアップ内に直書きしていました。一部、.mdファイルで作成していたページもありますが、レイアウトのためのユーティリティクラスを付与したHTMLタグが登場する状態でした。

具体的には、次のコードのような状態です。

src/pages/jamstack/index.astro(リニューアル前)

<h3 class="heading3">Jamstackの定義</h3>
<div class="stack-gap-20">
  <p>次のような特徴を持つWebアプリ、WebサイトがJamstackです。</p>
  <ul class="list2">
    <li>CDN/ADNで配信できる静的なHTMLがベース</li>
    <li>動的なコンテンツはJavaScriptにより、APIを通じて取得し表示</li>
    <li>フロントエンドとサーバーサイドは完全分離</li>
  </ul>
</div>

これでは、コンテンツを変更したいとなった場合に、スタイルを維持するためのマークアップやクラス指定まで気を配る必要があります。

コンテンツの編集は、サイトをメインで開発したエンジニアが行うとは限らないので、HTMLタグを意識せず書けるのが理想です。そして、スタイルはコンポーネントに閉じ込め、CSSも意識せずに書きたいのです。

また、リニューアル版会社サイトでは、ページ内目次や、別なページ内の特定のセクションへのリンクをナビゲーションとしてまとめたページがあります。これらを実現するにあたって、各見出しにid属性を付与する必要がありました。

Astroでは、MarkdownやMDXを使うことで、自動的に見出しにidが付与されます。もはや、Markdown(MDX)を使わない理由がありません。

前掲のコードと同様のコンテンツ(一部改訂しています)は、リニューアル版サイトでは次のようなMDXで管理しています。

src/content/pages/jamstack/index.mdx(リニューアル後)

## Jamstackの定義

:::MediumSection{icon="circle" iconSize="L"}

### JamstackといえるWebアプリ、Webサイト

- CDNやJamstackで使われるホスティングサービスで配信できる静的なHTMLがベース
- 事前にデータを埋め込んだ静的HTMLを生成するので高速に配信
- 動的なコンテンツはJavaScriptにより、APIを通じて取得し表示

:::

おおむね通常のMarkdown記法ですが、:::という見慣れない記法がありますね。この独自の拡張記法については、次回詳しく解説します。

ページのメタ情報管理をコンテンツコレクションで改善する

前節のコード例で、ファイルがsrc/pages配下ではなく、src/content/pages配下に置かれていることに気づいた方もいるかもしれません。

リニューアル前は、src/pages配下に各ページの.astroファイルを置くという、素直なファイルベースルーティングに沿っていました。

今回のリニューアルでは、各ページのMDXファイルをsrc/content/pages配下に設置し、コンテンツコレクションで管理するように変更しています。

src/content/pages配下のディレクトリ構造はURLの階層構造に合わせ、src/pages/[...slug].astroファイル内で、コンテンツコレクションとして各ページのコンテンツやメタ情報を取得し、ディレクトリ構造をもとにルーティングを実装しています。

このようなコンテンツコレクションでページを管理する方式に変更したのは、ページのタイトルや概要といったメタ情報を活用しやすくするためです。

ページを.astroファイルで記述する場合、metaタグに埋め込むタイトルや概要などは、---で囲まれたスクリプト部分で定義することが考えられます。

src/pages/jamstack/index.astro

---
const title = 'Jamstackとは?'
const description = 'ピクセルグリッドがオススメするJamstackについて解説します。'
---

<BaseLayout title={title} description={description}>
  <!-- 本文 -->
</BaseLayout>

しかし、このようにAstroコンポーネントにメタ情報を埋め込んでしまうと、他のページでこの情報を使うことはできません。

たとえば、ビルド時にタイトルを埋め込んだページごとのOGP画像を一括生成したい場合や、サイトマップのような一覧ページを作成したい場合などに対応できないのです。

各ページのメタ情報をJSON形式で管理(リニューアル前)

この問題を解消する苦肉の策として、リニューアル以前はsrc/pages配下の各ディレクトリに_meta.jsonというファイルを設置し、各ページのメタ情報をJSON形式で管理していました。

src/pages/jamstack/_meta.json

[
  {
    "url": "/jamstack/",
    "title": "Jamstackとは?",
    "description": "ピクセルグリッドがオススメするJamstackについて解説します。"
  },
  {
    "url": "/jamstack/modernweb/",
    "title": "これまでのWebとの比較",
    "description": "Webサーバーの利用を前提としたこれまでのWebと\nJamstackの構成を比較します。",
    "ogcategory": "Jamstackとは?"
  },
  {
    "url": "/jamstack/prerendering/",
    "title": "事前にデータを埋め込む",
    "description": "事前にレンダリングすることで多くのメリットが得られる\nJamstackの手法を解説します。",
    "ogcategory": "Jamstackとは?"
  },
  {
    "url": "/jamstack/dynamic/",
    "title": "高度なUIと動的データ",
    "description": "Jamstackで動的なデータを取り扱う方法を解説します。",
    "ogcategory": "Jamstackとは?"
  },
  {
    "url": "/jamstack/services/",
    "title": "モダンなサービスを活用",
    "description": "Jamstackと組み合わせて使われるサービスを紹介します。",
    "ogcategory": "Jamstackとは?"
  }
]

しかし、どうしても二重管理になってしまったり、メタ情報一覧を得たい場合は各ディレクトリに設置された_meta.jsonを走査する処理を自前で書く必要があったりと、決してスマートとはいえない状態でした。

コンテンツコレクションでページを管理(リニューアル後)

コンテンツコレクションでページを管理することで、メタ情報と実際のページのコンテンツを一つのファイルで管理しながらも、他のファイルからメタ情報にアクセスすることが容易になります。

src/content/pages/jamstack/index.mdx

---
name: Jamstackとは?
description: ピクセルグリッドがオススメするJamstackについて解説します。
---

{/* 本文 */}

.astroファイルだけでなく、カスタムエンドポイントでもコンテンツコレクションのデータを取得できるので、たとえばビルド時にページのメタ情報を一覧化したファイルを自動生成することも可能です。

リニューアルした会社サイトでは、まさにこの方法で自動生成したファイルを元に、ページごとに異なるOGP画像を自動生成しています。

次のコードが、実際にメタ情報一覧ファイルの生成に使われているものです。

src/pages/build-data/og-meta.json.ts

import { getCollection } from "astro:content";

export const GET = async () => {
  // pagesコレクションのデータを全件取得
  const pages = await getCollection("pages");
  // Frontmatterのメタ情報を取り出してオブジェクト化
  // ここでは、{ [slug]: title }という形式に整形している
  const metas = pages.reduce((acc, page) => {
    return {
      ...acc,
      [page.slug]: page.data.name,
    };
  }, {});
  // ビルド時にJSONファイルを生成する(SSGの場合)
  return new Response(JSON.stringify(metas, null, 2));
};

pagesコレクションからすべてのデータを取り出し、slug(ページのURLスラッグ)をキー、data.name(ページのタイトル)を値としたレコード形式のJSONをgetエンドポイントで返しています。

/build-data/og-meta.jsonにアクセスしたり、fetchしたりすると、次のようなJSONが得られます。

/build-data/og-meta.json

{
  // << 省略 >>
  "jamstack/dynamic": "高度なUIと動的データ",
  "jamstack": "Jamstackとは?",
  "jamstack/modernweb": "これまでのWebとの比較",
  "jamstack/prerendering": "事前にデータを埋め込む",
  "jamstack/services": "モダンなサービスを活用",
  "news": "ニュース",
  "news/events": "イベント",
  "news/media": "メディア",
  "news/topics": "会社からのお知らせ",
  "news/topics/2017/relocation": "オフィス移転のお知らせ",
  "news/topics/2020/covid-19": "感染拡大防止に向けた当社の対応",
  // << 省略 >>
}

このデータからどのようにしてOGP画像を自動生成しているか、その仕組みについては、次回以降で詳しく解説します。

データベースとしてのコンテンツコレクション

ここまで、ページのコンテンツをコンテンツコレクションで管理することで、ページ固有の情報にアクセスしやすくなることを解説してきました。

私たちはさらに、なるべくデータを特定のページに依存させないことにも注力しています。

データの独立

たとえば、スタッフのデータは会社情報のスタッフのページで使っているだけではありません。概要ページで表示する従業員数をカウントするためにも使っています。

スタッフのようなデータも、コンテンツコレクションとして管理することで、どのページからでもアクセスできるようにしています。

ただし、表示に関する情報は持たせたくないため、MDXではなく、コンポーネントを使うことができないMarkdownファイルを使用します。

src/content/staff/oosugi.md

---
name:
  - 大杉
  - 
ruby:
  - おおすぎ
  - まこと
job:
  title: シニアディレクター
  category: director
image: https://www.pxgrid.com/cdn-cgi/image/width=320,height=320,format=auto,quality=80/assets/images/profile/oosugi.jpg
accounts:
  twitter: "oosugi20"
  github: "oosugi20"
blog:
  title: "oosugi20-blog"
  url: "http://oosugi20-blog.tumblr.com/"
px: 14
---

主にテクニカルディレクターとして、制作工程全体の効率化を行いながら、数百サイトのコーディングに携わる。またそれらの経験を活かしセミナー講師も勤めている。漠然としたイメージや状況をうまく把握し、最適なアドバイスや提案につなげることが得意。

著書に『[Web制作者のためのGit入門](https://www.amazon.co.jp/dp/4839952027/?tag=pxgrid-22)』(共著:マイナビ、2014年6月28日)がある。

このようにページから独立したデータとしておくと、たとえば、今後スタッフごとに個別のページを作ろうとなった場合にも対応できるでしょう。

データの関連づけ

コンテンツコレクションでは、リレーショナルデータベースのように、他のコレクションのデータを参照することも想定されています。

たとえば、得意とする開発をまとめたstrengthコレクションのFrontmatterには、関連する開発実績を表すdevcaseコレクションのslugを配列で指定する、devcaseフィールドを設置しています。

src/content/strength/media-site.md

---
title: メディアサイト
description: 高速でアクセスに強いメディアサイトを作るにはJamstackでの開発が最適です。
icon: media
devcase:
  - chichi
  - codegrid
company:
  - 株式会社 致知出版社
---

strengthコレクションのスキーマ定義では、reference("devcase")によってdevcaseコレクションに存在するデータのslugのみ許容するようスキーマを定義しています。

src/schema/strength.ts

import { reference, z } from "astro:content";

export const strengthSchema = z.object({
  title: z.string(),
  description: z.string().optional(),
  icon: z.string(),
  devcase: z.array(reference("devcase")).default([]),
  company: z.string().array().default([]),
});

コンテンツコレクションのgetEntryメソッドを使うことで、devcaseコレクションのslugから、実際のデータを取得することが可能です。

「メディアサイト」の開発実績を取得する

const strength = await getEntry("strength", 'media-site');
const devcases = await Promise.all(strength.data.devcase.map(async (ref) => {
  return await getEntry("devcase", ref.slug);
}))

階層や順序が重要なデータとYAML

コンテンツコレクションでは、階層や順序が重要となるデータの管理が悩ましくなります。

たとえば、登壇イベントの一覧は、「xx年」の「yy月」のような階層を持ち、さらに「zz日」のような順序も重要です。

src/content/events配下のディレクトリ構造で階層を表現することもできますが、入れ子になるとデータへのアクセスが面倒です。

入れ子になっているとフィルタリングに手間がかかる

const events2021 = await getCollection("events", (event) => {
  return event.id.startsWith("2021");
});

また、順序を担保するにはソートが必要ですが、日付でソートしようにも、次のような例外表記に対応できません。

細かい日付が不明

date: 2010-11

開始日と終了日の範囲で表されている

date:
  start: "2012-05-30"
  end: "2012-06-01"

このようなデータは、最小単位でファイルを分けることはせず、ある程度のまとまりで一つのYAML(もしくはJSON)ファイルとし、その中で階層や順序を表現するようにしています。

src/content/events/2023.yaml

- date: "2023-07-14"
  date_kind: 開催
  title: DIST.40 「Jamstackの実際とミライ」
  url: https://dist.connpass.com/event/284922/
  details:
    - title: Jamstackで実装したセゾンカードWebサイトを2年運用お手伝いしてみて
      person:
        - 高津戸 壮
      category: 講演
- date: "2023-07-11"
  date_kind: 開催
  title: Google I/O Extended Japan 2023
  url: https://developersonair.withgoogle.com/events/ioextendedjapan2023
  details:
    - title: "パネルディスカッション「#AskWeb: CSS」"
      person:
        - 矢倉 眞隆
      category: パネリスト
- date: "2023-03-01"
  date_kind: 開催
  title: TechFeed Experts Night#14 〜 絶対役立つ!最先端のCSS総ざらい
  url: https://techfeed.io/events/techfeed-experts-night-14
  details:
    - title: CSS Animation パフォーマンスを考える原理
      person:
        - 菅野 亜実
      category: 発表

ニュースのように更新頻度の高いファイルは、ダブルクォートやブラケットなどを書くのが面倒なJSONではなく、YAMLを採用しています。

これまでは、YAMLを扱うには@rollup/plugin-yamlのようなライブラリが別途必要でしたが、コンテンツコレクションでYAMLファイルがサポートされたことで、その必要もなくなりました。

まとめ

今回のリニューアルでは、Astroのコンテンツコレクションを最大限活用する前提でデータ設計を行ったことで、さまざまなメリットが得られました。

特に、コンテンツコレクションの中でMDXを使うことで、本来のMDXの機能だけではなく、外部ファイルから容易にアクセス可能なFrontmatterが使えるようになることは大きな魅力です。

しかし、MDXの自由度の高さや、記法の簡潔さゆえに悩ましいと感じた点も多くあります。

次回は、サイトを構成するデータの中でもページのコンテンツ管理に焦点を絞り、MDXでページを構築する上でこだわった点について、詳しく解説していきます。