Gatsby

React + GraphQLベースの成熟したSSG。豊富なプラグインエコシステムと55k超のGitHubスターを持つ。

言語:JavaScript/TypeScript
フレームワーク:React
ビルド速度:Medium
GitHub Stars:55k
初回リリース:2015
人気ランキング:第3位

トレンド・動向

Jamstackムーブメントの牽引役として確立された地位。マーケティングサイトやブログで継続的に採用。

# 静的サイトジェネレータ Gatsby ## 概要 Gatsbyは「React + GraphQLベースの成熟したSSG」として開発された、Jamstackムーブメントの牽引役として確立された地位を持つ静的サイトジェネレータです。55k超のGitHubスターを誇り、豊富なプラグインエコシステムと自動的なパフォーマンス最適化により、マーケティングサイトやブログ構築に継続的に採用されています。React開発者にとって馴染み深いコンポーネントベースの開発体験と、GraphQLによる統一的なデータ管理により、企業レベルのWebサイト構築を効率化します。 ## 詳細 Gatsby 5.x版は2015年の初回リリースから培った豊富な実績により、React SSGの確固たる地位を維持しています。自動コード分割、画像最適化、重要スタイルのインライン化、遅延読み込み、リソースのプリフェッチなど、パフォーマンス最適化が自動実行される点が最大の特徴。GraphQLデータレイヤーにより、Markdown、CMS、API等の多様なデータソースを統一的に管理可能です。Slices API、部分ハイドレーション(Beta)、DSG(Deferred Static Generation)、SSRなど、ページ単位での柔軟なレンダリング選択により、パフォーマンスと生産性の両立を実現します。 ### 主な特徴 - **自動パフォーマンス最適化**: コード分割、画像最適化、プリフェッチの自動実行 - **GraphQLデータレイヤー**: 統一的なクエリインターフェースによる複数データソース管理 - **豊富なプラグインエコシステム**: 3000+の公式・コミュニティプラグイン - **Reactコンポーネントベース**: 既存React知識の完全活用 - **複数レンダリング戦略**: SSG、DSG、SSRのページ単位選択 - **CDNホスティング対応**: サーバーレスで低コスト運用可能 ## メリット・デメリット ### メリット - Reactエコシステムでの圧倒的な成熟度と豊富な学習リソース - 自動パフォーマンス最適化による高速サイト生成と運用効率 - GraphQLによる強力で柔軟なデータ管理・統合機能 - 3000超のプラグインによる拡張性と機能豊富性 - WordPress、Contentful、Strapi等の主要CMSとの優れた統合 - Netlify、Vercel、Gatsby Cloudでの最適化されたデプロイ体験 ### デメリット - 2024年以降の開発活動低下と事実上のメンテナンス停止状態 - 大規模サイトでのビルド時間増大とメモリ使用量増加 - GraphQL学習コストと小規模サイトでの過剰な複雑性 - Next.js等の競合フレームワークと比較した機能追加の遅れ - プラグイン依存による互換性問題とバージョン管理の複雑化 - 将来性への懸念と新規プロジェクトでの採用リスク ## 参考ページ - [Gatsby 公式サイト](https://www.gatsbyjs.com/) - [Gatsby ドキュメント](https://www.gatsbyjs.com/docs/) - [Gatsby GitHub リポジトリ](https://github.com/gatsbyjs/gatsby) ## 書き方の例 ### インストールとプロジェクト作成 ```bash # Gatsby CLIのインストール npm install -g gatsby-cli # 新しいプロジェクト作成 gatsby new my-gatsby-site cd my-gatsby-site # 開発サーバー起動 gatsby develop # プロダクションビルド gatsby build # ビルド結果のプレビュー gatsby serve ``` ### 基本的なページコンポーネント ```jsx // src/pages/index.js import React from "react" import { graphql } from "gatsby" import Layout from "../components/layout" import SEO from "../components/seo" const IndexPage = ({ data }) => { const { site } = data return ( <Layout> <SEO title="ホーム" /> <div> <h1>ようこそ {site.siteMetadata.title} へ</h1> <p>最新のテクノロジーで構築されたウェブサイトです。</p> <section> <h2>私たちの特徴</h2> <ul> <li>高速なページ読み込み</li> <li>SEO最適化</li> <li>モバイルフレンドリー</li> </ul> </section> </div> </Layout> ) } export default IndexPage // ページクエリでサイトメタデータを取得 export const query = graphql` query { site { siteMetadata { title description author } } } ` ``` ### レイアウトコンポーネントとStaticQuery ```jsx // src/components/layout.js import React from "react" import { useStaticQuery, graphql } from "gatsby" import Header from "./header" import Footer from "./footer" import "./layout.css" const Layout = ({ children }) => { const data = useStaticQuery(graphql` query SiteTitleQuery { site { siteMetadata { title description social { twitter github } } } } `) const { title, description, social } = data.site.siteMetadata return ( <> <Header siteTitle={title} /> <div style={{ margin: `0 auto`, maxWidth: 960, padding: `0 1.0875rem 1.45rem`, }} > <main>{children}</main> </div> <Footer description={description} social={social} /> </> ) } export default Layout ``` ### GraphQLクエリとデータ取得 ```jsx // src/pages/blog.js import React from "react" import { graphql, Link } from "gatsby" import Layout from "../components/layout" import SEO from "../components/seo" const BlogPage = ({ data }) => { const posts = data.allMarkdownRemark.edges return ( <Layout> <SEO title="ブログ" /> <div> <h1>ブログ記事一覧</h1> {posts.map(({ node }) => { const title = node.frontmatter.title || node.fields.slug return ( <article key={node.fields.slug}> <header> <h3> <Link to={node.fields.slug}> {title} </Link> </h3> <small>{node.frontmatter.date}</small> </header> <section> <p dangerouslySetInnerHTML={{ __html: node.frontmatter.description || node.excerpt, }} /> </section> </article> ) })} </div> </Layout> ) } export default BlogPage export const query = graphql` query { allMarkdownRemark( sort: { fields: [frontmatter___date], order: DESC } ) { edges { node { excerpt fields { slug } frontmatter { date(formatString: "MMMM DD, YYYY") title description tags } } } } } ` ``` ### プラグイン設定とgatsby-config.js ```javascript // gatsby-config.js module.exports = { siteMetadata: { title: `私のGatsbyサイト`, description: `Gatsbyで構築した高速なウェブサイト`, author: `@example`, siteUrl: `https://example.com`, social: { twitter: `example`, github: `example`, }, }, plugins: [ `gatsby-plugin-react-helmet`, `gatsby-plugin-image`, `gatsby-plugin-sharp`, `gatsby-transformer-sharp`, { resolve: `gatsby-source-filesystem`, options: { name: `images`, path: `${__dirname}/src/images`, }, }, { resolve: `gatsby-source-filesystem`, options: { name: `blog`, path: `${__dirname}/content/blog`, }, }, { resolve: `gatsby-transformer-remark`, options: { plugins: [ { resolve: `gatsby-remark-images`, options: { maxWidth: 630, }, }, `gatsby-remark-prismjs`, `gatsby-remark-copy-linked-files`, `gatsby-remark-smartypants`, ], }, }, { resolve: `gatsby-plugin-google-analytics`, options: { trackingId: `ADD YOUR TRACKING ID HERE`, }, }, { resolve: `gatsby-plugin-manifest`, options: { name: `私のGatsbyサイト`, short_name: `GatsbySite`, start_url: `/`, background_color: `#663399`, theme_color: `#663399`, display: `minimal-ui`, icon: `src/images/gatsby-icon.png`, }, }, `gatsby-plugin-offline`, ], } ``` ### 動的ページ生成(gatsby-node.js) ```javascript // gatsby-node.js const path = require(`path`) const { createFilePath } = require(`gatsby-source-filesystem`) // スラッグフィールドの追加 exports.onCreateNode = ({ node, getNode, actions }) => { const { createNodeField } = actions if (node.internal.type === `MarkdownRemark`) { const slug = createFilePath({ node, getNode, basePath: `pages` }) createNodeField({ node, name: `slug`, value: slug, }) } } // 動的ページの作成 exports.createPages = async ({ graphql, actions, reporter }) => { const { createPage } = actions // ブログ投稿テンプレート const blogPostTemplate = path.resolve(`./src/templates/blog-post.js`) // 全Markdownファイルを取得 const result = await graphql(` { allMarkdownRemark( sort: { fields: [frontmatter___date], order: ASC } limit: 1000 ) { edges { node { id fields { slug } frontmatter { title } } } } } `) if (result.errors) { reporter.panicOnBuild(`GraphQLクエリでエラーが発生しました`) return } const posts = result.data.allMarkdownRemark.edges // 各投稿のページを作成 posts.forEach((post, index) => { const previousPostId = index === 0 ? null : posts[index - 1].node.id const nextPostId = index === posts.length - 1 ? null : posts[index + 1].node.id createPage({ path: post.node.fields.slug, component: blogPostTemplate, context: { id: post.node.id, previousPostId, nextPostId, }, }) }) // タグページの作成 const tagTemplate = path.resolve(`./src/templates/tag.js`) const tags = new Set() posts.forEach(post => { if (post.node.frontmatter.tags) { post.node.frontmatter.tags.forEach(tag => tags.add(tag)) } }) tags.forEach(tag => { createPage({ path: `/tags/${tag}/`, component: tagTemplate, context: { tag, }, }) }) } ``` ### 高度な最適化とパフォーマンス設定 ```jsx // src/templates/blog-post.js import React from "react" import { graphql } from "gatsby" import { GatsbyImage, getImage } from "gatsby-plugin-image" import Layout from "../components/layout" import SEO from "../components/seo" import Bio from "../components/bio" const BlogPostTemplate = ({ data, location }) => { const post = data.markdownRemark const siteTitle = data.site.siteMetadata?.title || `Title` const { previous, next } = data // 画像の最適化処理 const featuredImage = getImage(post.frontmatter.featuredImage) return ( <Layout location={location} title={siteTitle}> <SEO title={post.frontmatter.title} description={post.frontmatter.description || post.excerpt} image={featuredImage} /> <article className="blog-post" itemScope itemType="http://schema.org/Article" > <header> <h1 itemProp="headline">{post.frontmatter.title}</h1> <p>{post.frontmatter.date}</p> {featuredImage && ( <GatsbyImage image={featuredImage} alt={post.frontmatter.title} style={{ marginBottom: `2rem` }} /> )} </header> <section dangerouslySetInnerHTML={{ __html: post.html }} itemProp="articleBody" /> <hr /> <footer> <Bio /> </footer> </article> <nav className="blog-post-nav"> <ul style={{ display: `flex`, flexWrap: `wrap`, justifyContent: `space-between`, listStyle: `none`, padding: 0, }} > <li> {previous && ( <Link to={previous.fields.slug} rel="prev"> ← {previous.frontmatter.title} </Link> )} </li> <li> {next && ( <Link to={next.fields.slug} rel="next"> {next.frontmatter.title} → </Link> )} </li> </ul> </nav> </Layout> ) } export default BlogPostTemplate export const query = graphql` query BlogPostBySlug( $id: String! $previousPostId: String $nextPostId: String ) { site { siteMetadata { title } } markdownRemark(id: { eq: $id }) { id excerpt(pruneLength: 160) html frontmatter { title date(formatString: "MMMM DD, YYYY") description tags featuredImage { childImageSharp { gatsbyImageData( width: 1200 placeholder: BLURRED formats: [AUTO, WEBP, AVIF] ) } } } } previous: markdownRemark(id: { eq: $previousPostId }) { fields { slug } frontmatter { title } } next: markdownRemark(id: { eq: $nextPostId }) { fields { slug } frontmatter { title } } } ` // Deferred Static Generation (DSG) の設定 export async function config() { return ({ params }) => { // 古い記事をDSGで遅延生成 return { defer: shouldDeferGeneration(params.slug), } } } function shouldDeferGeneration(slug) { // 1年以前の記事は遅延生成 const oneYearAgo = new Date() oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1) // 実際の実装では記事の日付をチェック return checkPostDate(slug) < oneYearAgo } ```